SwipeRefreshLayout源码分析+自定义UC头条下拉刷新Demo

首先来看SwipeRefreshLayout(以下简称SR)的继承关系

image

NestedScrollingParent:嵌套滑动父接口

NestedScrollingChild :嵌套滑动子接口

Android 就是通过这两个接口, 来实现 子View 与父View 之间的嵌套滑动

  • NestedScrollingChild:源码
public interface NestedScrollingChild {
    /**
     * Enable or disable nested scrolling for this view
     * 为这个视图启用或禁用嵌套滚动
     */
    public void setNestedScrollingEnabled(boolean enabled);

    /**
     * Returns true if nested scrolling is enabled for this view.
     * 若启动嵌套滑动,则返回True
     */
    public boolean isNestedScrollingEnabled();

    /**
     * Begin a nestable scroll operation along the given axes.
     * 在给定的轴上开始一个新的滚动操作。
     * ViewCompat.SCROLL_AXIS_HORIZONTAL 横向
     * ViewCompat.SCROLL_AXIS_VERTICAL 纵向
     */
    public boolean startNestedScroll(int axes);

    /**
     * Stop a nested scroll in progress.
     * 停止嵌套的滚动
     */
    public void stopNestedScroll();

    /**
     * Returns true if this view has a nested scrolling parent.
     * 如果该视图有一个嵌套滚动的父视图,则返回true。
     */
    public boolean hasNestedScrollingParent();

    /**
     * Dispatch one step of a nested scroll in progress.
     *
     * 在处理滑动之后 调用 
     * @param dxConsumed x轴上 被消费的距离 
     * @param dyConsumed y轴上 被消费的距离 
     * @param dxUnconsumed x轴上 未被消费的距离 
     * @param dyUnconsumed y轴上 未被消费的距离 
     * @param offsetInWindow view 的移动距离
     * 如果事件被发送,则返回true,如果该事件不能被发送,则为false
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    /**
     * Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
     *
     *一般在滑动之前调用, 在ontouch 中计算出滑动距离, 然后调用该方法, 就给支持的嵌套的父View 处理滑动事件 
     * @param dx x 轴上滑动的距离, 相对于上一次事件, 不是相对于 down事件的 那个距离 
     * @param dy y 轴上滑动的距离 
     * @param consumed 一个数组, 可以传 一个空的 数组,  表示 x 方向 或 y 方向的事件 是否有被消费 
     * @param offsetInWindow   支持嵌套滑动到额父View 消费 滑动事件后 导致 本 View 的移动距离 
     * @return 支持的嵌套的父View 是否处理了 滑动事件
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    /**
     * Dispatch a fling to a nested scrolling parent.
     * @param velocityX x 轴上的滑动速度 
     * @param velocityY y 轴上的滑动速度 
     * @param consumed 是否被消费 
     * @return 
     */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    /**
     * Dispatch a fling to a nested scrolling parent before it is processed by this view.
     *
     *@param velocityX x 轴上的滑动速度 
     * @param velocityY y 轴上的滑动速度 
     * @return
     * @param velocityX Horizontal fling velocity in pixels per second
     * @param velocityY Vertical fling velocity in pixels per second
     * @return true if a nested scrolling parent consumed the fling
     */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
  • NestedScrollingParent源码:

public interface NestedScrollingParent {
    /**
     * React to a descendant view initiating a nestable scroll operation, claiming thenested scroll operation if appropriate.
     * 对嵌套滚动的子View进行响应
     * 
     *
     * @param child ViewParent包含触发嵌套滚动的view的对象
     * @param target触发嵌套滚动的view(在这里如果不涉及多层嵌套的话,child和ta   rget)是相同的
     * @param nestedScrollAxes 方向  ViewCompat.SCROLL_AXIS_HORIZONTAL
     *                         ViewCompat.SCROLL_AXIS_VERTICAL
     * @return true 如果ViewParent接受嵌套滚动操作,则返回true
     */
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    /**
     * React to the successful claiming of a nested scroll operation.
     * 对成功的使用嵌套滚动操作作出反应
     * @param child ViewParent包含触发嵌套滚动的view的对象
     * @param target触发嵌套滚动的view(在这里如果不涉及多层嵌套的话,child和ta   rget)是相同的
     * @param nestedScrollAxes 滑动的方向          ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *  ViewCompat#SCROLL_AXIS_VERTICAL
     */
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    /**
     * React to a nested scroll operation ending.
     * 对一个嵌套滚动操作的结果进行响应
     * @param target 启动滚动的View
     */
    public void onStopNestedScroll(View target);

    /**
     * React to a nested scroll in progress.
     * 对正在进行的嵌套滚动进行响应
     * @param target 控制滚动的子View
     * @param dxConsumed x轴消费的距离
     * @param dyConsumed y轴消费的距离
     * @param dxUnconsumed x轴未消费的距离
     * @param dyUnconsumed y轴未消费的距离
     */
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    /**
     * React to a nested scroll in progress before the target view consumes a portion of the scroll.
     *
     * @param target 控制滚动的子View
     * @param dx x轴消费总距离
     * @param dy y轴消费总距离
     * @param consumed Output. 父布局分别在x,y轴消费的总距离:consumed[0],
     consumed[1]
     */
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    /**
     * Request a fling from a nested scroll.
     * 嵌套滑动的速度
     * @param target 控制滚动的子View
     * @param velocityX velocityX x 轴上的滑动速度 
     * @param velocityY y 轴上的滑动速度
     * @param consumed 子view是否消费
     * @return true if this parent consumed or otherwise reacted to the fling
     */
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    /**
     * React to a nested fling before the target view consumes it.
     *
     * @param target 控制滚动的子View
     * @param velocityX x 轴上的滑动速度 
     * @param velocityY y 轴上的滑动速度
     * @return 如果父布局在这之前消费了该事件则返回True
     */
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    /**
     * Return the current axes of nested scrolling for this NestedScrollingParent.返回一个当前滑动轴,以下3种情况
     * @return Flags indicating the current axes of nested scrolling
     * @see ViewCompat#SCROLL_AXIS_HORIZONTAL
     * @see ViewCompat#SCROLL_AXIS_VERTICAL
     * @see ViewCompat#SCROLL_AXIS_NONE
     */
    public int getNestedScrollAxes();
}
这两个接口的作用在上面的注释中有详细的解释,下面就是最关键的SR源码的分析;因为SR继承的是ViewGroup,我们平常都会自定义View,而自定义View通常都少不了:onMeasure(测量),onDraw(绘画);而自定义ViewGroup会涉及到对子View的排版问题,所以在自定义ViewGroup中多了一个onLayout()方法需要我们处理,这些基本的问题解决后,若自定义控件涉及到触摸事件,也会需要我们对触摸事件的分发机制有一定的了解;然后就让我们根据SR源码来一步一步分析下拉刷新控件是怎样实现的!(对源码的分析都是以代码的注释的形式来进行的)
  • SR构造:
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        //ViewConfiguration定义UI中用于超时、大小和距离的标准常量和获取他们的值的方法
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
      //获取动画时间
        mMediumAnimationDuration = getResources().getInteger(
                android.R.integer.config_mediumAnimTime);
       //若没有任何绘图,则设置此方法(没有重写onDrow方法)
        setWillNotDraw(false);
        //设置减速插值器
        mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
        // 描述一个显示的一般信息的结构,例如它的大小、密度和字体大小。
        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
        //创建头部刷新控件
        createProgressView();
        //告诉ViewGroup是否按照该方法定义的顺序绘制它的孩子
        ViewCompat.setChildrenDrawingOrderEnabled(this, true);
        // the absolute offset has to take into account that the circle starts at an offset
        mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);
        mTotalDragDistance = mSpinnerOffsetEnd;
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);

        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);

        mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
        //头部刷新控件起始位置
        moveToStart(1.0f);

        final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
        setEnabled(a.getBoolean(0, true));
        a.recycle();
    }
在构造方法中主要做了一下几件事情:
  • 对一些常量(列如动画时间,圆的直径,圆的偏移量等)的设置
  • 将一个头部刷新控件加入进来
  • 创建mNestedScrollingParentHelper,mNestedScrollingChildHelper等对象,为这个视图启用嵌套滚动

onMeasure 方法:三件事

  • 找出目标View
  • 测量子控件的大小
  • 得到下拉刷新View的Index
 @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //mTarget:手势拖动的目标View
        if (mTarget == null) {
        //将不是头部刷新的View赋给mTarget
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
        //根据测量规格测出目标View的大小
        mTarget.measure(MeasureSpec.makeMeasureSpec(
                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
        //同上
        mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));
        mCircleViewIndex = -1;
        // Get the index of the circleview.
        for (int index = 0; index < getChildCount(); index++) {
            if (getChildAt(index) == mCircleView) {
                mCircleViewIndex = index;
                break;
            }
        }
    }
在得到各个子控件的大小后,就是对各个控件的排版问题,也就是 onLayout()方法
  • 确定目标View的位置:child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
  • 确定刷新控件的位置:mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
    (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
 @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();
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
        int circleWidth = mCircleView.getMeasuredWidth();
        int circleHeight = mCircleView.getMeasuredHeight();
        mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
                (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
    }
因为下拉刷新控件是在一开始的时候是不显示的,所以就要考虑各个子控件的绘制顺序,将下拉刷新控件放在最后绘制,getChildDrawingOrder用于返回当前迭代子视图的索引.就是说获取当前正在绘制的视图索引. 如果需要改变ViewGroup子视图绘制的顺序,则需要重载这个方法.(我试了一下,不重写好像也没问题)
 @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        if (mCircleViewIndex < 0) {
            return i;
        } else if (i == childCount - 1) {
            // Draw the selected child last
            return mCircleViewIndex;
        } else if (i >= mCircleViewIndex) {
            // Move the children after the selected child earlier one
            return i + 1;
        } else {
            // Keep the children before the selected child the same
            return i;
        }
    }
然后就是触摸事件分发机制;onInterceptTouchEvent():onInterceptTouchEvent是在ViewGroup里面定义的,该方法决定了事件到底交给谁处理 。
  • 当return true时,表示ViewGroup自己来处理onTouchEvent事件,子View接收不到onTouchEvent事件
  • 当return false时,表示ViewGroup不拦截事件,直接交给子View处理

onTouchEvent:

  • onTouchEvent只有当onInterceptTouchEvent返回true的时候才执行。它根据下拉的距离,动态的修改headerView的位置,通过调用setTargetOffsetTopAndBottom调用invalidate()方法进行重绘。
 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    //一大堆根据当前状态判断是否拦截触摸事件的逻辑
    //就是根据是否是最后一个条目或者是第一个条目进行事件拦截
       ....
    }
@Override
    public boolean onTouchEvent(MotionEvent ev) {
        ...

            case MotionEvent.ACTION_MOVE: {
                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    return false;
                }

                final float y = ev.getY(pointerIndex);
                startDragging(y);

                if (mIsBeingDragged) {
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    if (overscrollTop > 0) {
                        moveSpinner(overscrollTop);
                    } else {
                        return false;
                    }
                }
                break;
            }

        ...  

        case MotionEvent.ACTION_UP: {
                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                    return false;
                }

                if (mIsBeingDragged) {
                    final float y = ev.getY(pointerIndex);
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    mIsBeingDragged = false;
                    finishSpinner(overscrollTop);
                }
                mActivePointerId = INVALID_POINTER;
                return false;
            } 
    }

onTouchEvent方法中最重要的便是moveSpinner(overscrollTop)和finishSpinner(overscrollTop)方法的调用

  • 获取拖拽百分比和高度差并修正
  • 开启动画
  • 动态修正下拉刷新控件的位置
  • 设置监听

  • moveSpinner

 @SuppressLint("NewApi")
    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 ? mSpinnerOffsetEnd - mOriginalOffsetTop
                : mSpinnerOffsetEnd;
        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 */);
    }
  • finishSpinner
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) {
                    }

                };
            }
            //这个方法是进行下拉刷新的回复在ANIMATE_TO_START_DURATION=200毫秒内
            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
            mProgress.showArrow(false);
        }
    }

总结

到此我们的分析基本结束了,让我们一起来看看做了多少事情才写出一个下拉刷新控件

  • addView() 加入下拉刷新控件
  • 测量各个子view的大小
  • onLayout()对子view的位置进行确定以及确定各子view的绘制顺序
  • 触摸事件的分发机制
  • 嵌套滑动实现
  • 设置回调接口
下拉刷新控件的机制我们了解的差不多了,下面就是我们定制自己的下拉刷新,上拉加载控件了——仿UC头条下拉刷新布局
ps:上面也说了楼主数学太差,那个圆的角度变化和下拉距离偏移量的关系式对楼主来说太难了,所以就搞了个假的!不多说了,来看下效果图:

这里写图片描述

这只是加深对自定义ViewGroup的理解而做的一个小Demo,下面是代码地址

Github>>>,大家随便看看就行,推荐一个很酷炫的下拉刷新第三方,楼主就是看了这位大神写的下拉刷新控件才想看看原理是怎样的!
酷炫的下拉刷新上拉加载控件》》》

  • 若有错误,敬请指正!!!

拼搏在技术道路上的一只小白And成长之路

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值