Android下拉刷新控件SwipeRefreshLayout源码浅析



转载地址:http://blog.csdn.net/u011443509/article/details/52017355


SwipeRefreshLayout是Android官方的下拉刷新控件,使用简单,界面美观,不熟悉的朋友可以随便搜索了解一下,这里就不废话了,直接进入正题。
首先给张流程图吧,标出了几个主要方法的作用,可以结合着看一下哈。


这种下拉刷新控件的原理不难,基本就是监听手指的运动,获取手指的坐标,通过计算判断出是哪种操作,然后就是回调相应的接口了。SwipeRefreshLayout是继承自ViewGroup的,根据Android的事件分发机制,触摸事件应该是先传递到ViewGroup,根据onInterceptTouchEvent的返回值决定是否拦截事件的,那么就onInterceptTouchEvent出发:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
@Override  
    public boolean onInterceptTouchEvent(MotionEvent ev) {  
        ensureTarget();  
  
        final int action = MotionEventCompat.getActionMasked(ev);  
  
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {  
            mReturningToStart = false;  
        }  
  
        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;  
                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;  
        }  
  
        return mIsBeingDragged;  
    }  
是否拦截的情况有很多种,这里如果满足五个条件之一就直接返回false,使用时触摸事件发生冲突的话就可以从这里出发分析,这里也不具体展开了。简单看一下,在ACTION_DOWN中记录下手指坐标,ACTION_MOVE中计算出移动的距离,并且判断是否大于阈值,是的话就将mIsBeingDragged标志位设为true,ACTION_UP中则将mIsBeingDragged设为false。最后返回的是mIsBeingDragged。
SwipeRefreshLayout一般是嵌套可滚动的View使用的,正常滚动时会满足前面的条件,这时不进行拦截,只有当滚动到顶部才会进入后面action的判断。在手指按下和抬起期间mIsBeingDragged为true,也就是说进行拦截,接下来就是如何处理了,看看onTouchEvent:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
@Override  
    public boolean onTouchEvent(MotionEvent ev) {  
          
        ....  
  
        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) {  
                        moveSpinner(overscrollTop);  
                    } else {  
                        return false;  
                    }  
                }  
                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;  
    }  
这里省略了一些代码,前面还有几行跟上面的类似,也是在满足其中一个条件时直接返回;switch中也还有几行处理多指触控的,这些都略过了。看一下ACTION_MOVE中计算了手指移动的距离,这时的mIsBeingDragged正常情况下应为true,当距离大于零就会执行moveSpinner。在ACTION_UP中则会执行finishSpinner,到这里就可以猜出,执行刷新的逻辑主要就在这两个方法中。
看这两个方法前,要知道两个重要的成员变量:一个是mCircleView,是CircleImageView的实例,继承了ImageView,主要绘制进度圈的背景;另一个是mProgress,是MaterialProgressDrawable的实例,继承自Drawable且实现Animatable接口,主要绘制进度圈,SwipeRefreshLayout正是通过调用其方法来绘制动画。接下来就先看一下moveSpinner:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
<span style="font-size:18px;">  
[java] view plain copy 在CODE上查看代码片派生到我的代码片
private void moveSpinner(float overscrollTop) {  
        mProgress.showArrow(true);  
        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;  
  
        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 (mScale) {  
            setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance));  
        }  
        if (overscrollTop < mTotalDragDistance) {  
            if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA  
                    && !isAnimationRunning(mAlphaStartAnimation)) {  
                // Animate the alpha  
                startProgressAlphaStartAnimation();  
            }  
        } else {  
            if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {  
                // Animate the alpha  
                startProgressAlphaMaxAnimation();  
            }  
        }  
        float strokeStart = adjustedPercent * .8f;  
        mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));  
        mProgress.setArrowScale(Math.min(1f, adjustedPercent));  
  
        float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;  
        mProgress.setProgressRotation(rotation);  
        setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);  
    }</span>  
showArrow是显示箭头,中间那一坨主要也是一些math和设置进度圈的样式,倒数第二行执行了setProgressRotation,传入的是经过一堆计算后的rotation,这堆计算主要是优化效果,比如在刚开始移动时增长比较快,超过刷新的距离后就增长比较慢。传入该方法后,mProgress就根据它来绘制进度圈,因此主要的动画就应该在这个方法内。最后一行执行setTargetOffsetTopAndBottom,我们来看一下:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
<span style="font-size:18px;">private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) {  
        mCircleView.bringToFront();  
        mCircleView.offsetTopAndBottom(offset);  
        mCurrentTargetOffsetTop = mCircleView.getTop();  
        if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {  
            invalidate();  
        }  
    }</span>  


比较简单,就是调整进度圈的位置并进行记录。最后来看一下finishSpinner:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
<span style="font-size:18px;">private void finishSpinner(float overscrollTop) {  
        if (overscrollTop > mTotalDragDistance) {  
            setRefreshing(true, true /* notify */);  
        } else {  
            // cancel refresh  
            mRefreshing = false;  
            mProgress.setStartEndTrim(0f, 0f);  
            Animation.AnimationListener listener = null;  
            if (!mScale) {  
                listener = new Animation.AnimationListener() {  
  
                    @Override  
                    public void onAnimationStart(Animation animation) {  
                    }  
  
                    @Override  
                    public void onAnimationEnd(Animation animation) {  
                        if (!mScale) {  
                            startScaleDownAnimation(null);  
                        }  
                    }  
  
                    @Override  
                    public void onAnimationRepeat(Animation animation) {  
                    }  
  
                };  
            }  
            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);  
            mProgress.showArrow(false);  
        }  
    }</span>  
逻辑也很简单,当移动的距离超过设定值时就执行setRefreshing(true,true),在该方法里更新一些成员变量的值后会执行animateOffsetToCorrectPosition,由名字就知道是执行动画将进度圈移动到正确位置的(也就是头部)。如果移动的距离没有超过设定值,就会执行animateOffsetToStartPosition。一起看一下animateOffsetToCorrectPosition和animateOffsetToStartPosition这两个方法:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
<span style="font-size:18px;">private void animateOffsetToCorrectPosition(int from, AnimationListener listener) {  
        mFrom = from;  
        mAnimateToCorrectPosition.reset();  
        mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION);  
        mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);  
        if (listener != null) {  
            mCircleView.setAnimationListener(listener);  
        }  
        mCircleView.clearAnimation();  
        mCircleView.startAnimation(mAnimateToCorrectPosition);  
    }  
  
    private void animateOffsetToStartPosition(int from, AnimationListener listener) {  
        if (mScale) {  
            // Scale the item back down  
            startScaleDownReturnToStartAnimation(from, listener);  
        } else {  
            mFrom = from;  
            mAnimateToStartPosition.reset();  
            mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION);  
            mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);  
            if (listener != null) {  
                mCircleView.setAnimationListener(listener);  
            }  
            mCircleView.clearAnimation();  
            mCircleView.startAnimation(mAnimateToStartPosition);  
        }  
    }</span>  
逻辑基本相同,进行一些设置后,最后都会执行mCircleView的startAnimation,只是传入的值以及监听器不同。
如果是要执行刷新的操作,传入的值是头部高度,监听器为:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
<span style="font-size:18px;">private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {  
        @Override  
        public void onAnimationStart(Animation animation) {  
        }  
  
        @Override  
        public void onAnimationRepeat(Animation animation) {  
        }  
  
        @Override  
        public void onAnimationEnd(Animation animation) {  
            if (mRefreshing) {  
                // Make sure the progress view is fully visible  
                mProgress.setAlpha(MAX_ALPHA);  
                mProgress.start();  
                if (mNotify) {  
                    if (mListener != null) {  
                        mListener.onRefresh();  
                    }  
                }  
                mCurrentTargetOffsetTop = mCircleView.getTop();  
            } else {  
                reset();  
            }  
        }  
    };</span>  
动画完成后,也就是进度圈移动到头部后,会执行mProgress.start();这里执行的就是在刷新时进度圈转啊转的动画。接下来注意到如果mListener不为空就会执行onRefresh方法,这个mListener其实就是执行setOnRefreshListener所设置的监听器,因此在这里完成刷新。如果是执行回到初始位置的操作,传入的值为初始高度(也就是顶部之上),监听器为
[java] view plain copy 在CODE上查看代码片派生到我的代码片
<span style="font-size:18px;">listener = new Animation.AnimationListener() {  
  
  
    @Override  
    public void onAnimationStart(Animation animation) {  
    }  
  
  
    @Override  
    public void onAnimationEnd(Animation animation) {  
        if (!mScale) {  
            startScaleDownAnimation(null);  
        }  
    }  
  
  
    @Override  
    public void onAnimationRepeat(Animation animation) {  
    }  
  
  
};</span>  
移动到初始位置后会执行startScaleDownAnimation,也就是消失的动画了,到这里整个刷新流程就结束了。
这样就基本把SwipeRefreshLayout的流程过了一遍,但是要实现这样一个控件还是有很多小问题需要考虑的,这里主要是把思路理清,知道如果出现问题该怎样解决。另外从源码也可以看出swipeRefreshLayout的定制性是比较差的,也不知道google是不是故意这样希望以后全都用这种统一样式的下拉刷新。。当然有一些第三方下拉刷新的定制性还是比较好的,使用上也不难。但是有些人(比如我)是比较倾向于使用官方的控件的,不到万不得已都不想用第三方工具。下次会写一篇探讨一下用swipeRefreshLayout实现自定义样式的文章~
后续还有一篇从修改swipeRefreshLayout的源码出发自定义样式高仿微信朋友圈的下拉刷新效果的文章,有兴趣可以看一下哈http://blog.csdn.net/u011443509/article/details/52025019
希望随手点个赞支持一下新人哈~
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值