SwipeRefreshLayout源码分析

一,概述

在上篇blog中我们发现使用SwipeRefreshLayout实现下拉刷新极其方便快捷,而且拉刷新控件与展示内容的控件实现了分离,非常便于代码的扩展与维护。但是其刷新UI确实很简单,很难入公司设计师的法眼,这时就需要我们修改刷新UI了。

这篇blog从源码的角度分析SwipeRefreshLayout的实现原理,即为了学习SwipeRefreshLayout的实现思想,也可以实现自己想要的刷新UI。

SwipeRefreshLayout是一个view类,继承ViewGroup。其中的核心方法有:
构造方法;
onMeasure();
onLayout();
onIntercepterTouchEvent();(重点)
onTouchEvent();(重点)
下面按照以上方法的顺序逐渐分析源码。

二,构造方法

构造方法的核心代码如下:

    public SwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

      /**触发移动事件的最小距离,自定义View处理touch事件的时候,有的时候需要判断用户是否真的存在movie,系统提供了这样的方法。表示滑动的时候,手的移动要大于这个返回的距离值才开始移动控件。*/
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        //获取移动动画的差值器
        mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);

        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density);//得到刷新View的宽度
        mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density);//得到刷新View的高度

        createProgressView();//创建刷新View
    }

createProgressView方法的源码如下:

    private void createProgressView() {
        mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2);
        mProgress = new MaterialProgressDrawable(getContext(), this);
        mProgress.setBackgroundColor(CIRCLE_BG_LIGHT);
        mCircleView.setImageDrawable(mProgress);
        mCircleView.setVisibility(View.GONE);
        addView(mCircleView);//将mCircleView添加到父View上。
    }

注:构造方法主要做了两件事情:
1,使用addView添加刷新的view
2,初始化参数,刷新view的大小,最大滑动距离等等。

三,onMeasure方法

方法的核心代码是:

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTarget == null) {
            ensureTarget();//找到mTarget,这个view既是承载数据的view,可能是ListView,ScrollView,RecyclerView。
        }
        if (mTarget == null) {
            return;
        }
        //测量mTarget
        mTarget.measure(MeasureSpec.makeMeasureSpec(
                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
         //测量刷新View    
        mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
    }

注:onMeasure方法中没有特别的注意点,就是找到两个子view,并测量。

四,onLayout方法

方法的核心代码是:

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int width = getMeasuredWidth();
        final int height = getMeasuredHeight();
        final View child = mTarget;
        final int childLeft = getPaddingLeft();
        final int childTop = getPaddingTop();
        final int childWidth = width - getPaddingLeft() - getPaddingRight();
        final int childHeight = height - getPaddingTop() - getPaddingBottom();

        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);//布局mTarget
        int circleWidth = mCircleView.getMeasuredWidth();
        int circleHeight = mCircleView.getMeasuredHeight();
        mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
                (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);//布局刷新View
    }

注意:刷新的view的位置与mCurrentTargetOffsetTop有关。

五,onIntercepterTouchEvent方法

这个方法是刷新功能实现的重点。如果说onMeasure 方法和onLayout方法只是自定义View的知识,那么onIntercepterTouchEvent方法就是刷新功能实现的核心。一定要注意这个方法。
方法的核心代码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();//找到mTarget对象
        final int action = MotionEventCompat.getActionMasked(ev);//获取手势动作

        /**下面几种情况直接返回false,表示不拦截。其中canChildScrollUp非常重要,后面会详细讲解这个方法。*/
        if (!isEnabled() || mReturningToStart || canChildScrollUp()|| mRefreshing || mNestedScrollInProgress) {
            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;//这行代码是关键,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;
    }

注意:onIntercepterTouchEvent方法中决定事件拦截不拦截,如果不拦截就传递给mTarget,由mTarget消费事件。
下面再次看下不拦截的代码:

 if (!isEnabled() || mReturningToStart || canChildScrollUp()|| mRefreshing || mNestedScrollInProgress) {
   return false;
 }

以上五种情况都不拦截,其中最重要的是canChildScrollUp方法。这个方法表示mTarget是否可以向下滑动,方法的源码是:

    public boolean canChildScrollUp() {
        if (android.os.Build.VERSION.SDK_INT < 14) {//首先做了一个判断,判断sdk的版本。
            if (mTarget instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) mTarget;
                return absListView.getChildCount() > 0
                        && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                                .getTop() < absListView.getPaddingTop());
            } else {
                return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
            }
        } else {//当前的sdk版本肯定是大于14的,所以只看这行代码。
            return ViewCompat.canScrollVertically(mTarget, -1);//这行代码判断view在竖直方向是否可以滑动,-1表示是否可以向下滑动。
        }
    }

当mTarget可以向下滑动时则不拦截,只有mTarget滑动到顶部时,此时不能向下滑动了,此时拦截。拦截之后调用自己的onTouchEvent方法,交给自己处理。

六,onTouchEvent方法

我们知道当view拦截事件后,会调用onTouchEvent方法。
onTouchEvent方法的核心代码是:

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        int pointerIndex = -1;
        /**下面几种情况直接返回false,表示不处理*/
        if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) {
            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) {
                        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;
    }

下面首先看重点一,moveSpinner方法,这个方法的作用时移动刷新view的位置。在move事件时被调用
方法的核心源码如下:

    private void moveSpinner(float overscrollTop) {//参数overscrollTop表示这一瞬间手指移动的距离
        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);//计算targetY的值,即刷新View将要到达的目标位置。
        float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
        mProgress.setProgressRotation(rotation);//设置刷新view的动画
        setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);//改变刷新View的位置,这个方法也是重点
    }

setTargetOffsetTopAndBottom方法的源码如下:

    private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) {
        mCircleView.bringToFront();
        mCircleView.offsetTopAndBottom(offset);//移动mCircleView的位置,这个方法会自动调用invalidate方法,所以此时onLayout方法会被调用
        mCurrentTargetOffsetTop = mCircleView.getTop();
        if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
            invalidate();
        }
    }

下面首先看重点二,finishSpinner方法,这个方法的作用是处理手指抬起以后的操作。
方法的核心源码如下:

    private void finishSpinner(float overscrollTop) {
        if (overscrollTop > mTotalDragDistance) {//如果滑动的距离大于某个值则调用刷新方法
            setRefreshing(true, true /* notify */);
        } else {//如果滑动的距离不大于某个值,则回到原始的位置。
            mRefreshing = false;
            mProgress.setStartEndTrim(0f, 0f);
            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
            mProgress.showArrow(false);
        }
    }

刷新方法是setRefreshing,下面看这个方法的源码:

    private void setRefreshing(boolean refreshing, final boolean notify) {
        if (mRefreshing != refreshing) {
            mNotify = notify;
            ensureTarget();
            mRefreshing = refreshing;
            if (mRefreshing) {//此时mRefreshing等于true,所以会走这一步,
                animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
            } else {
                startScaleDownAnimation(mRefreshListener);
            }
        }
    }

animateOffsetToCorrectPosition方法表示以动画的形式回到某个位置,动画不重要,重要的是动画监听器mRefreshListener,下面看这个对象的创建。

    private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {}
        public void onAnimationRepeat(Animation animation) {}
        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();
            }
        }
    };

这是一个动画监听器,当动画结束时调用mListener.onRefresh(),这个即是我们设置的刷新监听器。此时就调用了刷新的方法。

七,总结

经过以上源码分析,我们得到的结论如下:
1,SwipeRefreshLayout是一个viewGroup,里面有两个子view,一个是刷新view,即mCircleView,即在刷新时旋转的view。另外一个时显示内容的View,即mTarget,这个就是Listview,或者Scrollview.
2,在onIntercepterTouchEvent方法中做了事件的拦截,当listView可以向下滑动时不拦截,事件由ListViw处理,当不能向下滑动时拦截事件,事件交给SwipeRefreshLayout当onTouchEvent方法处理。
3,onTouchEvent方法分别对move事件和up事件进行了处理,当满足刷新条件时掉用刷新监听器的方法。

八,扩展使用

看过SwipeRefreshLayout源码后,就可以实现自己想要的刷新控件了。
SuperEasyRefreshLayout是一个仿照SwipeRefreshLayout的实现原理实现的一个强大的下拉刷新控件,不仅美化了刷新UI,而且可以根据需要自定义UI效果的View,且实现了上拉加载更多的功能。
SuperEasyRefreshLayout的使用介绍的地址是:http://blog.csdn.net/fightingxia/article/details/75307875
SuperEasyRefreshLayout的源码在GitHub上的地址是:https://github.com/guozhengXia/SuperEasyRefreshLayout

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值