想必你们也发现,时下的不少App都应用了这个Google出品的SwipeRefreshLayout下拉刷新控件,它以Material Design风格、适用场景普遍,简单易用等特性而独步江湖。但在咱们使用的过程当中,不可避免地会发现一些bug,或者须要添加某些特性来知足需求。出现这些问题,最好的方法就是解读源码,理解它实现的原理,而且在理解源码的基础上修改源码,达成需求。然而不知为什么,至今尚未一篇关于SwipeRefreshLayout源码解析的文章,因此萌发了要写一篇这样的文章。鉴于阅读技术博文的枯燥,加之仍是篇源码解析的文章,我不打算一会儿扔出来一大段代码让读者去啃,而是一步一步往下走,揭开SwipeRefreshLayout的神秘面纱。java
为何源码广泛都很难读,有人甚至谈之色变?其实代码(出自大神之手)生来是易读的,但代码多了就变得难读了。因此阅读源码时,要把握住主干,细枝末节能够暂时忽略,一路下来理解了程序工做流程后再回过头来会有一种豁然开朗的感受。
阅读源码我仍是选择Android Studio。这个强大的工具提供了不少快捷键,大大地方便了源码的阅读。android
在看往下看以前,我但愿你了解:git
所幸该控件没有跟系统api耦合,因此能够直接copy一份代码到本身的demo工程中,尽情地改。可是hint会理解报出一些错误。首先包名要改一下,类名最好也改吧,以避免混淆~其次把CircleImageView和MaterialProgressDrawable这两个类都copy过来,放在同一个包里。如图:
若是嫌麻烦能够直接fork个人项目。github
咱们朝着未知的黑暗出发。打开SwipeRefreshTestLayout的类文件,看到左边这么小的滑块,其实我一开始是拒绝的~ 感受无从下手啊有没有… 沉下心来,想一想看看它是继承于ViewGroup的,因此想一想它必定有两个很关键的方法:onMeasure和onLayout,分别解决了它和它的子View占多大地和搁到哪。由于它是一个下拉刷新控件,它一定要涉及到事件分发的处理,一样是两个关键方法:onInterceptTouchEvent和onTouchEvent,分别用于决定是否拦截点击事件和进行点击事件的处理。天空瞬间亮了许多…api
@Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } //mTarget的尺寸为match_parent,除去内边距 mTarget.measure(MeasureSpec.makeMeasureSpec( getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); //设置mCircleView的尺寸 mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); //若是mOriginalOffsetTop未被初始化而且mUsingCustomStart ?,则将下拉小圆的初始位置设置成默认值 if (!mUsingCustomStart && !mOriginalOffsetCalculated) { mOriginalOffsetCalculated = true; mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); } mCircleViewIndex = -1; // Get the index of the circleview.获取circleview的索引值,主要是为了后面重载getChildDrawingOrder时要用 for (int index = 0; index < getChildCount(); index++) { if (getChildAt(index) == mCircleView) { mCircleViewIndex = index; break; } } }
咱们看到,这个方法代码不长,但却很关键。重写该方法的做用是设置子View的尺寸。出现mTarget是什么未知生物?其实就是一个它包裹的子View,一般是ListView等一些可滚动的控件。ensureTarget();保证它非空并存在。若是不当心包裹了多个VIew呢?则mTarget就是其中的最后一个子View。mCircleView又是什么生物呢?顾名思义,下拉的白色小圆圈,一个ImageView而已。mCurrentTargetOffsetTop 和mOriginalOffsetTop 是两个很是关键的变量,分别表示当前mCircleView的位置(top值)和初始时mCircleView的位置(top值),固然它们初始化都等于mCircleView高度的负数。还有一个mUsingCustomStart 是什么呢?我当时也不知道。不要紧,Ctrl+F11打个书签,等读完再回头看。或者咱们能够经过Alt+F7看看它的在哪里被引用过。
能够看到,它在setProgressViewOffset被赋值为true,而该方法是用于设置CircleView初始的位置和刷新停留的位置,Custom是自定义的意思,因此mUsingCustomStart就是一个标志,表示是否用自定义的起始位置,而默认的起始位置就是CircleView高度的负数。框架
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); if (getChildCount() == 0) { return; } if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } final View child = mTarget; final int childLeft = getPaddingLeft(); final int childTop = getPaddingTop(); final int childWidth = width - getPaddingLeft() - getPaddingRight(); final int childHeight = height - getPaddingTop() - getPaddingBottom(); //将mTarget放在覆盖parent的位置(除去内边距) child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); //将mCircleView放在mTarget的平面位置上面居中,初始化时是彻底隐藏在屏幕外的 int circleWidth = mCircleView.getMeasuredWidth(); int circleHeight = mCircleView.getMeasuredHeight(); mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); }
这个方法代码也不长,很简单,但却很关键。做用是安排子View的位置。将mTarget填充整个控件,将mCircleView放在mTarget的平面位置上面居中,初始化时是彻底隐藏在屏幕外的。ide
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); //若是当mCircleView正在返回初始位置的同时手指按下了,将标志mReturningToStart复位 if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } //若是下拉被禁用、mCircleView正在返回初始位置、mTarget没有到达顶部、 //正在刷新、mNestedScrollInProgress // 不拦截,不处理点击事件,处理权交还mTarget if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing || mNestedScrollInProgress) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { //手指按下时,记录按下的坐标 case MotionEvent.ACTION_DOWN: // setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; final float initialDownY = getMotionEventY(ev, mActivePointerId); if (initialDownY == -1) { return false; } mInitialDownY = initialDownY; break; case MotionEvent.ACTION_MOVE: if (mActivePointerId == INVALID_POINTER) { Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); return false; } final float y = getMotionEventY(ev, mActivePointerId); if (y == -1) { return false; } final float yDiff = y - mInitialDownY; //若是是滑动动做,将mIsBeingDragged置为true if (yDiff > mTouchSlop && !mIsBeingDragged) { mInitialMotionY = mInitialDownY + mTouchSlop; mIsBeingDragged = true; mProgress.setAlpha(STARTING_PROGRESS_ALPHA); } break; //处理多指触控 case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; //手指松开,将标志复位 case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; break; } //若是正在被拖拽,拦截该系列的点击事件,并调用本身的onTouchEvent()来处理 return mIsBeingDragged; }
这个方法的逻辑很是清晰。当若是下拉被禁用、mCircleView正在返回初始位置、mTarget没有到达顶部、
或者正在刷新时, 不拦截,不处理点击事件,处理权交还mTarget。排除以上状况后,还须要进一步判断。
当手指按下时,记录按下的坐标;在MotionEvent.ACTION_MOVE当中,判断是不是滑动动做,若是是,拦截该系列的点击事件,并调用本身的onTouchEvent()来处理。函数
重头戏来了!这个方法是关键中的关键:工具
@Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex = -1; if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } //若是被禁用、CircleView正在复位、没到达顶部、mNestedScrollInProgress,直接返回,不处理该事件 if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; break; case MotionEvent.ACTION_MOVE: { pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); //下拉的总高度 final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; if (mIsBeingDragged) { if (overscrollTop > 0) { //spinner可理解为下拉组件,将spinner移到指定的高度 //很关键的方法,进入看看 moveSpinner(overscrollTop); } else { return false; } } break; } //多指触控的处理 case MotionEventCompat.ACTION_POINTER_DOWN: { pointerIndex = MotionEventCompat.getActionIndex(ev); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index."); return false; } mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; //关键代码! case MotionEvent.ACTION_UP: { pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); //计算松开手时下拉的总距离 final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; mIsBeingDragged = false; //关键方法,进去看看 finishSpinner(overscrollTop); mActivePointerId = INVALID_POINTER; return false; } case MotionEvent.ACTION_CANCEL: return false; } return true; }
在case MotionEvent.ACTION_MOVE当中,计算下拉的总高度overscrollTop,DRAG_RATE是下拉阻尼,能够经过改变它的值来改变下拉手感哦~~而后进入到moveSpinner()方法,将spinner移到指定的高度。那么spinner是啥?其实就是下拉组件的意思。post
/** * 经过调用setTargetOffsetTopAndBottom()方法移动下拉组件Spinner(mCircleView) * 同时更新mProgress(一个drawable)的绘制进度 * @param overscrollTop 下拉高度 */ private void moveSpinner(float overscrollTop) { mProgress.showArrow(true);//显示Progressbar的箭头 //通过一系列的计算,spinner控制下拉的最大距离 float originalDragPercent = overscrollTop / mTotalDragDistance; float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset - mOriginalOffsetTop : mSpinnerFinalOffset; float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist); float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( (tensionSlingshotPercent / 4), 2)) * 2f; float extraMove = (slingshotDist) * tensionPercent * 2; //计算spinner将要(target)被移动到的位置对应的Y坐标,当targetY为0时,小圆圈恰好所有露出来 int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); // where 1.0f is a full circle if (mCircleView.getVisibility() != View.VISIBLE) { mCircleView.setVisibility(View.VISIBLE); } if (!mScale) { ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); } //如下这对if-else主要是在经过下拉进度,对mProgress在下拉过程设置颜色透明度,箭头旋转角度,缩放大小的控制 //若是下拉高度小于mTotalDragDistance(一个触发下拉刷新的高度) if (overscrollTop < mTotalDragDistance) { //若是支持下拉小圆圈缩放,设置颜色透明度和缩放大小 if (mScale) { setAnimationProgress(overscrollTop / mTotalDragDistance); } if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) { // Animate the alpha startProgressAlphaStartAnimation(); } float strokeStart = adjustedPercent * .8f; mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); mProgress.setArrowScale(Math.min(1f, adjustedPercent)); } else {//下拉高度达到了触发刷新的高度 if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // Animate the alpha startProgressAlphaMaxAnimation(); } ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); } float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; mProgress.setProgressRotation(rotation); //这是关键调用!动态设置mSpinner的高度。进入该函数看看 setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */); }
该方法主要干的事就是经过调用setTargetOffsetTopAndBottom()方法移动下拉组件Spinner(mCircleView),同时更新mProgress(一个drawable)的绘制进度。其中进行了一些复杂的运算,其实就是在控制下拉的最大高度,避免用户无限下拉…说明一下,这个mScale,由于我已经添加了javadoc,读者Ctrl+Q就能够查看它的相关信息。它以为spinner下拉过程是否支持缩放,能够经过setProgressViewEndTarget()和setProgressViewOffset()设置。但我发现一个bug,若是手指下拉过快,小圆就会来不及放到最大…小圆明显变小了,如图
好,有改bug的但愿就有了继续阅读的动力!咱们进入setTargetOffsetTopAndBottom()看看。
/** * 设置mCircleView的偏移量 * 同时更新mCurrentTargetOffsetTop * @param offset 偏移量,可正可负 * @param requiresUpdate 界面是否须要重绘invalidate(); */ private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { //bringToFront()该方法会调用requestLayout()和invalidate()把view放到前面 //已经重写了getChildDrawingOrder方法,因此没有必要再调用该方法了,我我的认为... //可经过手动调用invalidate()来代替它 // mCircleView.bringToFront(); mCircleView.offsetTopAndBottom(offset); mCurrentTargetOffsetTop = mCircleView.getTop(); if (requiresUpdate /*&& android.os.Build.VERSION.SDK_INT < 11*/) { invalidate(); } }
该方法很短,倒是mCircleView可以下拉的精髓所在啊!offsetTopAndBottom()本质上是调用layout(getLeft(),getTop()+offsetY,getRight(),getBottom()+offsetY);(注意不是onLayout())同时对mCircleView的top和bottom进行偏移,offset是View总体在垂直方向上的偏移量。这里我把bringToFront()注释掉,bringToFront()该方法会调用requestLayout()和invalidate()把view放到前面,由于已经重写了getChildDrawingOrder方法,因此没有必要再调用该方法了,我我的认为…可经过手动调用invalidate()来代替它。
到此,咱们已经了解过它下拉的的过程,下面进行回溯到onTouchEvent的case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_UP: { pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); //计算松开手时下拉的总距离 final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; mIsBeingDragged = false; //关键方法,进去看看 finishSpinner(overscrollTop); mActivePointerId = INVALID_POINTER; return false; }
计算完了松开手时下拉的总距离后,交给方法finishSpinner(overscrollTop);处理。进去看看。
/** * 手指松开后,处理下拉组件Spinner * 设置开始刷新的动画,或者 * 将下拉组件Spinner回滚隐藏 * @param overscrollTop 下拉距离 */ private void finishSpinner(float overscrollTop) { if (overscrollTop > mTotalDragDistance) {//下拉距离达到了可触发刷新的高度 //关键方法 setRefreshing(true, true /* notify */); } else {//下拉距离还未达到了可触发刷新的高度,作一些复位的操做 // cancel refresh mRefreshing = false; mProgress.setStartEndTrim(0f, 0f); //值得关注的是这个回滚动画 AnimationListener listener = null; if (!mScale) { listener = new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { if (!mScale) { startScaleDownAnimation(null); } } }; } //开始回滚动画 //这是一个比较复杂的方法,也是比较有用的方法 //其实这个本质上不是开启一个动画,而是一个数值产生器 //经过监听数值变化, //从mCurrentTargetOffsetTop这个高度开始, //调用setTargetOffsetTopAndBottom()慢慢回滚到mOriginalOffsetTop animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); mProgress.showArrow(false); } }
这里看到的mTotalDragDistance一样能够经过Ctrl+Q查看它的信息。这个方法只有一对大大的if-else,若是下拉距离达到了可触发刷新的高度,开始刷新;不然开始回滚动画,将Spinner回滚到开始的位置(也就是mOriginalOffsetTop)。而animateOffsetToStartPosition这个方法是一个内涵很丰富的方法,涉及到多步跳转才能了解完全。你们能够去github fork下来,找到相应方法Ctrl+左击进去看看,里面的方法都添加了详细的注释,相信你们必定能看懂。有朋友可能会问,这里怎么用视图动画而不用属性动画呢?其实这里并非开启一个真正意义上的动画,而是一个数值产生器,经过监听数值变化,从mCurrentTargetOffsetTop这个高度开始,调用setTargetOffsetTopAndBottom()慢慢回滚到mOriginalOffsetTop。
下面咱们一块儿来看看setRefreshing(true, true /* notify */);
/** * 设置刷新状态,该方法一般不是由类库使用者来调用,而是在用户下拉的时候由SwipeRefreshLayout来调用 * @param refreshing 是否刷新 * @param notify 是否回调onRefresh() */ private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshing != refreshing) { mNotify = notify; ensureTarget(); mRefreshing = refreshing; if (mRefreshing) {//启动刷新 animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); } else {//中止刷新 //开始Spinner消失动画 startScaleDownAnimation(mRefreshListener); } } }
该方法一般不是由类库使用者来调用,而是在用户下拉的时候由SwipeRefreshLayout本身来调用,因此它是private的。若是启动刷新,则调用animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);将mCircleView移到正确的高度(也就是mSpinnerFinalOffset),animateOffsetToCorrectPosition()跟上文提到的animateOffsetToStartPosition()方法的实现机理是彻底同样的。咱们这里回想一下,刚才的bug是因为手指松开时mCircleView的Scale值没有达到1,那么在这里咱们就能够在它的移动到刷新位置的动画结束时,把它的Scale手动设置为1。
private AnimationListener mRefreshListener = new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { if (mRefreshing) { //改bug ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); // Make sure the progress view is fully visible mProgress.setAlpha(MAX_ALPHA); mProgress.start(); if (mNotify) { if (mListener != null) { mListener.onRefresh(); } } } else { mProgress.stop(); mCircleView.setVisibility(View.GONE); setColorViewAlpha(MAX_ALPHA); // Return the circle to its start position if (mScale) { setAnimationProgress(0 /* animation complete and view is hidden */); } else { setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, true /* requires update */); } } mCurrentTargetOffsetTop = mCircleView.getTop(); } };
效果还可吧!
咱们发现onRefresh()是在这个被回调的,并且仅在这里被回调。
不知不觉,天亮了~框架脉络已经很清晰了吧。
还有一些变量或方法的名字带有NestedScroll没有提到,其实那是跟嵌套滑动有关的,不知道也不影响源码的阅读。
下面说说我遇到过的一个问题,当咱们在Activity的onCreate中
mRefreshLayout = (SwipeRefreshTestLayout) findViewById(R.id.refresh_widget); mRefreshLayout.setRefreshing(true);
new Handler().postDelayed(new Runnable() { @Override public void run() { mRefreshLayout.setRefreshing(true); } },100);
或者
mRefreshLayout.postDelayed(new Runnable() { @Override public void run() { mRefreshLayout.setRefreshing(false); } }, 3000);
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); mRefreshLayout.setRefreshing(true); }
public void setRefreshing(final boolean refreshing) { //防止类库使用者在SwipeRefreshLayout还没彻底被初始化时调用该方法 //仍是建议使用者重写Activity的onWindowFocusChanged()方法来调用setRefreshing(true); if (!isShown()&& refreshing){ Log.e("SwipeRefreshLayout", "It's not advisable to invoke setRefreshing() when SwipeRefreshLayout is inVisible."); new Handler().postDelayed(new Runnable() { @Override public void run() { setRefreshing(true); } },50/*该时间能够任意设置*/); return; } .....省略若干代码...... }
项目代码已上传至Github。—repo
点star和转发也是一种支持!
若是你发现有什么不清楚或不妥的地方欢迎留言讨论。