美文网首页
SwipeRefreshLayout - 源码解析

SwipeRefreshLayout - 源码解析

作者: ZHDelete | 来源:发表于2018-05-23 14:04 被阅读139次

转自:
hanks-zyh/SwipeRefreshLayout

SwipeRefreshLayout源码解析


SwipeRefreshLayout 是什么,这个不用多说,本篇主要是看源码,学习.

通过setRefreshing(true)setRefreshing(true)来手动调用触发和关闭刷新

注意:

onRefresh的回调,只有在手势下拉时,爱会触发,通过setRefreshing(true/false)只能控制刷新的动画是否显示(圈圈是否转转转).

SwipeRefreshLayout源码分析:

源码基于: appcompat-v7:27.1.1
extends ViewGroup implements NestedScrollingParent, NestedScrollingChild

java.lang.Object
   ↳    android.view.View
       ↳    android.view.ViewGroup
           ↳    android.support.v4.widget.SwipeRefreshLayout

SwipeRefreshLayout 的分析分为两个部分:自定义 ViewGroup 的部分,处理和子视图的嵌套滚动部分。

SwipeRefreshLayout extends ViewGroup

其实就是一个自定义的 ViewGroup ,结合我们自己平时自定义 ViewGroup 的步骤:

  • 初始化变量
  • onMeasure
  • onLayout
  • 处理交互 (dispatchTouchEvent onInterceptTouchEvent onTouchEvent)

接下来就按照上面的步骤进行分析。

1.初始化变量:

SwipeRefreshLayout 内部有 2 个 View,一个圆圈(mCircleView),一个内部可滚动的View(mTarget)。除了 View,还包含一个 OnRefreshListener 接口,当刷新动画被触发时回调。

preview.png

从 构造方法开始看:

/**
 * Constructor that is called when inflating SwipeRefreshLayout from XML.
 *
 * @param context
 * @param attrs
 */
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
    super(context, attrs);

    // 系统默认的最小滚动距离
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

    // 系统默认的动画时长 400ms
    mMediumAnimationDuration = getResources().getInteger(
            android.R.integer.config_mediumAnimTime);
    //开启 完整 绘制 模式
    setWillNotDraw(false);
    //圈圈 show hide 的动画插值器
    mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);

    //圈圈 直径 与 像素密度 换算
        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);

    // 创建刷新动画的圆圈
    createProgressView();
    //
    setChildrenDrawingOrderEnabled(true);

    // the absolute offset has to take into account that the circle starts at an offset
  // Default offset in dips from the top of the view to where the progress spinner should stop
  //绝对偏移量必须考虑到圆是从偏移量开始的
//圈圈 滑动的最大距离
    mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density;
    // 刷新动画的临界距离值
    mTotalDragDistance = mSpinnerFinalOffset;

    // 通过 NestedScrolling 机制来处理嵌套滚动
    mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
//nestal scroll enable 是 NestedScrollingChild 接口里的方法
    setNestedScrollingEnabled(true);
    //关于 圈圈 位置相关 onLayout时使用mCurrentTargetOffsetTop 的值
        mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
        moveToStart(1.0f);

    // 获取 xml 中定义的属性
    final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
    setEnabled(a.getBoolean(0, true));
    a.recycle();

}

创建刷新动画的圆圈:

private void createProgressView() {
        mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT);
        mProgress = new CircularProgressDrawable(getContext());
        mProgress.setStyle(CircularProgressDrawable.DEFAULT);
        mCircleView.setImageDrawable(mProgress);
        mCircleView.setVisibility(View.GONE);
        addView(mCircleView);
}

初始化的时候创建一个出来一个 View (下拉刷新的圆圈)。可以看出使用背景圆圈是 v4 包里提供的 CircleImageView 控件,中间的是 MaterialProgressDrawable 进度条。 另一个 View 是在 xml 中包含的可滚动视图。

2.onMeasure:
    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTarget == null) {
//初始化 mTarget 即内容部分
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
//测量mTarget
        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.计算 圈圈 在ViewGroup中的索引,给mCircleViewIndex赋值
        for (int index = 0; index < getChildCount(); index++) {
            if (getChildAt(index) == mCircleView) {
                mCircleViewIndex = index;
                break;
            }
        }
    }

这一步骤,完成了所有View 子View的测量过程 , 同时 初始化了mCircleViewIndex,为后面通过该值拿到圈圈 做准备

3.onLayout

 @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//onMeasure 完成,获得测量得到的 宽和高
        final int width = getMeasuredWidth();
        final int height = getMeasuredHeight();
        if (getChildCount() == 0) {
            return;
        }
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
//去除SwipeRefreshLayout的padding值
        final View child = mTarget;
        final int childLeft = getPaddingLeft();
        final int childTop = getPaddingTop();
        final int childWidth = width - getPaddingLeft() - getPaddingRight();
        final int childHeight = height - getPaddingTop() - getPaddingBottom();
//layout mTarget的位置
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
        int circleWidth = mCircleView.getMeasuredWidth();
        int circleHeight = mCircleView.getMeasuredHeight();
//layout圈圈 的位置,其中mCurrentTargetOffsetTop 决定了 top(和bottom)的位置
/*
构造方法里的初值是如下:即在屏幕上方(不可见),直径的距离
mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
*/

        mCircleView.layout(
                (width / 2 - circleWidth / 2), 
                mCurrentTargetOffsetTop,
                (width / 2 + circleWidth / 2),
                 mCurrentTargetOffsetTop + circleHeight);
    }

circle_position.png

在onLayout中放置了mCircleView(圈圈)的位置,注意,其top(顶部)位置是mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;,而-mCirDiameter是圆的直径取负数,是在屏幕上方不可见的

到此,经过onMeasure和onLayout,Swipelayout及其子View已经定他们的大小、位置,都在屏幕上正确展示了.

3. 处理与子视图的滚动交互

这一部分,才是重头戏,是重中之重!!!

下拉刷新控件的主要功能是档子视图下拉到最顶部是,继续下拉可以出现刷新动画.而子视图本身如果可以滚动的话,所有滚动事件又要都交给子视图. 借助Android提供的nestredScrolling机制,是的SwipeRefreshlayout很轻松的解决了与子视图的滚动冲突问题,SwipeRefreshLayout通过实现NestedScrollingParent和NestedScrollingChild接口来处理滚动冲突.
SwipeRefreshLayout作为Parent嵌套一个可以滚动的子视图,那么就需要了解一下nestedScrollingParent接口

我们先贴出注释文档和翻译:

/**
 * This interface should be implemented by {@link android.view.ViewGroup ViewGroup} subclasses
 * that wish to support scrolling operations delegated by a nested child view.
当你的ViewGroup希望支持由嵌套子视图委托的滚动操作,你就要实现这个接口
 *
 * <p>Classes implementing this interface should create a final instance of a
 * {@link NestedScrollingParentHelper} as a field and delegate any View or ViewGroup methods
 * to the <code>NestedScrollingParentHelper</code> methods of the same signature.</p>
实现这个接口的类,要创建一个final的NestedScrollingParentHelper变量,并把View or ViewGroup的方法都委托给Helper中的相同参数的方法
 *
 * <p>Views invoking nested scrolling functionality should always do so from the relevant
 * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility
 * shim static methods. This ensures interoperability with nested scrolling views on Android
 * 5.0 Lollipop and newer.</p>
调用嵌套滚动功能的视图应始终从相关的{@link ViewCompat},{@link ViewGroupCompat}或{@link ViewParentCompat}兼容静态方法执行此操作。 
这确保了与Android 5.0 Lollipop和更新的嵌套滚动视图的互操作性。

 */

下面是源码:

public interface NestedScrollingParent {
     /**
     * 当子视图调用 ViewCompat.startNestedScroll(View, int) 后调用该方法。返回 true 表示响应子视图的滚动。
     * 实现这个方法来声明支持嵌套滚动,如果返回 true,那么这个视图将要配合子视图嵌套滚动。当嵌套滚动结束时会调用到 onStopNestedScroll(View)。
     *
     * @param child 可滚动的子视图
     * @param target NestedScrollingParent 的直接可滚动的视图,一般情况就是 child
     * @param nestedScrollAxes 包含 ViewCompat#SCROLL_AXIS_HORIZONTAL, ViewCompat#SCROLL_AXIS_VERTICAL 或者两个值都有。
     * @return 返回 true 表示响应子视图的滚动。
     */
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);

     /**
     * 这一方法,在onStartNestedScroll() 返回true之后,被调用
     * 作用是: 让这个支持嵌套滑动的ViewGroup和它的父类来进行初始化相关的配置
     * 实现这个方法时,要调用其super方法,不可省略
     *
     * @param child 这个ViewGroup的直接子View,包括target
     * @param target  启动嵌套滑动的那个子View
     * @param axes 标志位,有如下两种情况:
     *     {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *     {@link ViewCompat#SCROLL_AXIS_VERTICAL} or
     *      both
     * @see #onStartNestedScroll(View, View, int)
     * @see #onStopNestedScroll(View)
     */
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);

       /**
     * 当嵌套滑动结束时,调用此方法
     *在嵌套滑动结束后,进行一下清理工作
     * 例如: 当一个嵌套滑动被ACTION_UP 或者 ACTION_CANCEL给结束了,就调用
    *此方法
    *实现这个方法的时候,必须要调用其super方法,如果存在super对象的话
     *
     * @param target 启动嵌套滑动那个View
     */
    void onStopNestedScroll(@NonNull View target);

    /**
     * React to a nested scroll in progress.
    * 嵌套滑动进行中,调用此方法
     *
     * <p>This method will be called when the ViewParent's current nested scrolling child view
     * dispatches a nested scroll event. 
    *当这个ViewParent的childView正在滑动时,调用此方法
    *To receive calls to this method the  ViewParent must have
     * previously returned <code>true</code> for a call to
     * {@link #onStartNestedScroll(View, View, int)}.</p>

     *当然,这个方法被调用的前提是: onStartnestScroll(View, View, int) 里返回了true

     * <p>Both the consumed and unconsumed portions of the scroll distance are reported to the
     * ViewParent. 
    *已经消耗的和没消耗的滑动距离,都会回调到此方法的参数中
    *An implementation may choose to use the consumed portion to match or chase scroll
     * position of multiple child elements,
  * 实现此方法时,可以使用"已经消耗的距离"来匹配或追踪多个子元素的滚动位置
     * for example. The unconsumed portion may be used to  allow continuous dragging of multiple 
     * scrolling or draggable elements, such as scrolling
     * a list within a vertical drawer where the drawer begins dragging once the edge of inner
     * scrolling content is reached.</p>
     * 例如: "未消耗的距离"可以用来允许连续拖动多个滚动或可拖动元素,比如 在一个垂直抽屉中有个滚动列表,
     * 在内部滚动内容达到边缘到达时,抽屉开始拖动。(译者注: 就行BottomNestScrollDialog那种)
     *
     * @param target The descendent view controlling the nested scroll
     * 这个嵌套滑动的发起者 即 内部的可滚动的子View
     * @param dxConsumed Horizontal scroll distance in pixels already consumed by target
     * 消耗的 x方向滚动
     * @param dyConsumed Vertical scroll distance in pixels already consumed by target
     * 未消耗的 x方向滚动
     * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by target
     * 消耗的 y方向滚动
     * @param dyUnconsumed Vertical scroll distance in pixels not consumed by target
     * 未消耗的y方向滚动
     */
    void onNestedScroll(@NonNull 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.
     * 当嵌套滑动进行之中,target消耗部分滑动之前,此方法被调用(译者注: 即滑动的那个子View,消耗滑动距离之前)
     *
     * <p>When working with nested scrolling often the parent view may want an opportunity
     * to consume the scroll before the nested scrolling child does. An example of this is a
     * drawer that contains a scrollable list. The user will want to be able to scroll the list
     * fully into view before the list itself begins scrolling.</p>
     * 在嵌套滑动中: paterntView 可能想要有机会在childView(target)消耗滑动之前,自己先消耗滑动,比如:
     * 这儿有个抽屉,抽屉含有一个可滚动list,用户可能希望在滑动是,先把抽屉完全全滑开之后,这个list再开始滚动,
     * 这个时候,就是这个方法派上用场的时候
     *
     * <p><code>onNestedPreScroll</code> is called when a nested scrolling child invokes
     * {@link View#dispatchNestedPreScroll(int, int, int[], int[])}. The implementation should
     * report how any pixels of the scroll reported by dx, dy were consumed in the
     * <code>consumed</code> array. Index 0 corresponds to dx and index 1 corresponds to dy.
     * This parameter will never be null. Initial values for consumed[0] and consumed[1]
     * will always be 0.</p>
     * 
     * 当嵌套滑动的target(即childView) 调用disptchNestedPreScroll(int, int, int[], int[])方法是,这个方法被调用
     * 实现这个方法时,应该上报:自己消耗了多少像素的滑动距离,通过把小号的值放在int[] consumed里面.
     * index 0 : x方向消耗的滑动距离
     * index 1 : y方向小号的滑动距离
     * 这个数组从 不会为null 初始值是[0,0]
     *
     * @param target View that initiated the nested scroll
     *               触发嵌套滑动的子View 
     * @param dx Horizontal scroll distance in pixels
     *           x方向滑动距离
     * @param dy Vertical scroll distance in pixels
     *           y方向滑动距离
     * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
     *                 我们要返回的数据,里面包含了,这个viewParent 消耗掉的 x和y方向的滑动距离
     */
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);

     /**
     * Request a fling from a nested scroll.
     * 在嵌套滑动中 请求消耗fling(手指抛滑)的动作
     *
     * <p>This method signifies that a nested scrolling child has detected suitable conditions
     * for a fling. Generally this means that a touch scroll has ended with a
     * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
     * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
     * along a scrollable axis.</p>
     * 这个方法表示: 嵌套滑动的childView(译者注:target) 检测到了一个fling动作,通常这意味着:
     * 在滑动方向上达到或超过了最小的FlingVelocity(判断是否fling的速度阈值)
     *
     * <p>If a nested scrolling child view would normally fling but it is at the edge of
     * its own content, it can use this method to delegate the fling to its nested scrolling
     * parent instead. The parent may optionally consume the fling or observe a child fling.</p>
     *如果一个嵌套滑动的childView(target) 想要正常的进行fling滑动,但是它(target)已经达到了自己的内容边缘
     *那么可以用此方法,把fling滑动委托给parentView,parentView就会有选择的消耗fling操作,或者就静静看着childView的fling滑动
     *
     * @param target View that initiated the nested scroll
     * @param velocityX Horizontal velocity in pixels per second
     *                  x方向滑动速度 像素/s
     * @param velocityY Vertical velocity in pixels per second
                       y方向滑动速度 像素/s
     * @param consumed true if the child consumed the fling, false otherwise
     *                 true -> target 消耗了fling滑动 false -> 没消耗
     * @return true if this parent consumed or otherwise reacted to the fling
     * 返回true: parent将要消耗fling
     *      false: 不消耗
     */
    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);


    /**
     * React to a nested fling before the target view consumes it.
     * 在target 对消耗掉fling滑动之前,回调到此方法
     *
     * <p>This method siginfies that a nested scrolling child has detected a fling with the given
     * velocity along each axis. Generally this means that a touch scroll has ended with a
     * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
     * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
     * along a scrollable axis.</p>
     * 这个方法表示:
     * 嵌套滑动的childView(target)检测到了一个fling滑动(速度为: velocityX,velocityY),
     *
     * <p>If a nested scrolling parent is consuming motion as part of a
     * {@link #onNestedPreScroll(View, int, int, int[]) pre-scroll}, it may be appropriate for
     * it to also consume the pre-fling to complete that same motion. By returning
     * <code>true</code> from this method, the parent indicates that the child should not
     * fling its own internal content as well.</p>
     * 如果如果前面 onNestedPreScroll 中父布局消耗了事件,那么这个方法也会回调
     * 返回ture: paentView表明:childView(target) 不应该fling它自己(即target自己)的内部内容
     *
     * @param target View that initiated the nested scroll
     * @param velocityX Horizontal velocity in pixels per second
     * @param velocityY Vertical velocity in pixels per second
     * @return true if this parent consumed the fling ahead of the target view
     * return true 表明parent在其child(target)之前,消耗了fling动作
     */
    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);

    /**
     * Return the current axes of nested scrolling for this NestedScrollingParent.
     * 返回当前 嵌套滚动,沿哪个坐标轴进行
     *
     * <p>A NestedScrollingParent returning something other than {@link ViewCompat#SCROLL_AXIS_NONE}
     * is currently acting as a nested scrolling parent for one or more descendant views in
     * the hierarchy.</p>
     * 么看懂,好像没什么用
     *
     * @return Flags indicating the current axes of nested scrolling
     * @see ViewCompat#SCROLL_AXIS_HORIZONTAL
     * @see ViewCompat#SCROLL_AXIS_VERTICAL
     * @see ViewCompat#SCROLL_AXIS_NONE
     */
    @ScrollAxis
    int getNestedScrollAxes();
}

上面是对NestScrollingParent的翻译,下面看一下SwipeRefreshlayout是如何实现这些方法的:

// NestedScrollingParent

// 子 View (target)开始滚动前回调此方法,返回 true 表示接 Parent 收嵌套滚动,然后调用 onNestedScrollAccepted
// 具体可以看 NestedScrollingChildHelper 的源码
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    // 子 View 回调,判断是否开始嵌套滚动 ,
    return isEnabled() && !mReturningToStart && !mRefreshing
            && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

@Override
 public void onNestedScrollAccepted(View child, View target, int axes) {
     // Reset the counter of how much leftover scroll needs to be consumed.
     mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);

     // ...省略代码
 }

对onStartNestedScroll(View child, View target, int nestedScrollAxes) 代码做解释说明:

return isEnabled() && !mReturningToStart && !mRefreshing
            && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;

什么时候会返回true?

  • 条件1: view是enable的
  • 条件2: target不能正在返回初始位置
    什么时候会返回初始位置: 刷新被取消或者刷新已经开始
  • 条件3: 没有正在刷新
  • 条件4: 是竖直方向的嵌套滚动

只有上面4项同时满足时,swipeRefresh才响应嵌套滑动,即才能下拉刷新

SwipeRefreshLayout 只接受竖直方向(Y轴)的滚动,并且在刷新动画进行中不接受滚动。

接下来看真正滚动的代码:onNestedPreScrollonNestedScroll
(PS: 按照正常人的思维逻辑,建议先关注onNestedScroll里的部分,因为此处是处理下拉,显示圈圈的过程)

// 子View(target) 在滚动的时候会触发, 看父类消耗了多少距离
//   * @param dx x 轴滚动的距离
//   * @param dy y 轴滚动的距离
//   * @param consumed 代表 父 View 消费的滚动距离 
//   * index0: x方向 
//   * index1: y方向
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {

    // dy > 0 表示手指在屏幕向上移动
    //  mTotalUnconsumed 表示子视图Y轴未消费的距离
    // 发生在 : 已经下拉了部分,此时不抬手,手指下向上滑动,想让圈圈回去,会触发此处代码
    if (dy > 0 && mTotalUnconsumed > 0) {

        if (dy > mTotalUnconsumed) {
            consumed[1] = dy - (int) mTotalUnconsumed;
 // SwipeRefreshLayout 就吧子视图位消费的距离全部消费
了。
//这里mTotalUnConsumed是已经向下滑动的距离,当向上滑动的dy大于已经向下移动的距离,
//多出的部分(即: 二者之差),也要消费掉,因此,在consumed[1] 里返回了这个差

            mTotalUnconsumed = 0;
        } else {
            mTotalUnconsumed -= dy; // 消费的 y 轴的距离
            consumed[1] = dy;
        }
        // 出现动画圆圈,并向上移动
        moveSpinner(mTotalUnconsumed);
    }

    // ... 省略代码
}


// onStartNestedScroll 返回 true 才会调用此方法。
// 此方法表示子View将滚动事件分发到父 View(SwipeRefreshLayout)
@Override
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
        final int dxUnconsumed, final int dyUnconsumed) {
    // ... 省略代码

    // 手指在屏幕上向下滚动,并且子视图不可以滚动(即子视图内容不能再向下移动)
    final int dy = dyUnconsumed + mParentOffsetInWindow[1];
    if (dy < 0 && !canChildScrollUp()) {
        mTotalUnconsumed += Math.abs(dy);
        moveSpinner(mTotalUnconsumed);
    }
}

SwipeRefreshLayout通过NestedScrollingParent接口完成了处理子视图嵌套滑动(同方向滑动冲突) 的问题,中间省略了一些处理: SwipeRefreshlayout作为child在其他支持嵌套滑动的父容器中的代码,这里不做深究
但是,下拉刷新需要根据手指在屏幕的状态(DOWN/MOVE/UP)来进行刷新动画,所以我们还要处理触摸事件:

首先是onInterceptTuchEvent,返回true,表示拦截事件:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
//去报 target 不为 null
        ensureTarget();

        final int action = ev.getActionMasked();
        int pointerIndex;
       //手指按下时 判断mReturningToStart,即,判断是否圈圈在返回top,(即是否下拉一半,取消,或者是否完整下拉已经触发refresh)
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }
    //以下几个状态,立刻返回false 不拦截
//1.eanble=false 2.圈圈在返回顶部 3.target仍然能上滑(手指上-下) 4. 正在刷新 5. 正在嵌套滑动(正在嵌套滑动是为什么不拦截: 因为拦截了,target就不能滑动了)

        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());
/*
由于pointer的index值在不同的MotionEvent对象中会发生变化,但是id值却不会变化。所以,当我们要记录一个触摸点的事件流时,就只需要保存其id,然后使用findPointerIndex(int)来获得其index值,然后再获得其他信息。
*/
          //将第一根手指,视为activie的手指 这里处理了多指下拉的情况
                mActivePointerId = ev.getPointerId(0);
                mIsBeingDragged = false;
                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
           // 记录手指按下的位置,用于判断 滑动距离
                mInitialDownY = ev.getY(pointerIndex);
                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;
                }

                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
                final float y = ev.getY(pointerIndex);
                startDragging(y);
                break;
/*
插入startDragging(y);便于理解
判断当拖动距离大于最小距离时设置 mIsBeingDragged = true;

    private void startDragging(float y) {
        final float yDiff = y - mInitialDownY;
        if (yDiff > mTouchSlop && !mIsBeingDragged) {
            mInitialMotionY = mInitialDownY + mTouchSlop;
            mIsBeingDragged = true;
// 正在拖动状态,更新圆圈的 progressbar 的 alpha 值
            mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
        }
    }

*/

            case MotionEvent.ACTION_POINTER_UP:
//多指下拉,此时抬起一根手指
                onSecondaryPointerUp(ev);
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
        }
    //可以看到,除了直接返回false的情况,
    //其余都是返回的mIsBeingDraged 这个标志位只有在MOVE时是true,即只在startDragging(float y)里是true 也即只有在拖动时才是true,即只有在拖动时才拦截
        return mIsBeingDragged;
    }

可以看到源码也就是进行简单处理,DOWN 的时候记录一下位置,MOVE 时判断移动的距离,返回值 mIsBeingDragged 为 true 时, 即 onInterceptTouchEvent 返回true,SwipeRefreshLayout 拦截触摸事件,不分发给 mTarget,然后把 MotionEvent 传给 onTouchEvent 方法。其中有一个判断子View的是否还可以滚动的方法 canChildScrollUp。
ps: 该方法是判断target 能否继续向上滑动(字面意思),即能否接受手指从上向下

    /**
     * @return Whether it is possible for the child view of this layout to
     *         scroll up. Override this if the child view is a custom view.
     */
    public boolean canChildScrollUp() {
        if (mChildScrollUpCallback != null) {
            return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
        }
        if (mTarget instanceof ListView) {
            return ListViewCompat.canScrollList((ListView) mTarget, -1);
        }
        return mTarget.canScrollVertically(-1);
    }

插入ViewCompat.canScrollList( ) 方法:

    public static boolean canScrollList(@NonNull ListView listView, int direction) {
        if (Build.VERSION.SDK_INT >= 19) {
            // Call the framework version directly
            return listView.canScrollList(direction);
        } else {
            // provide backport on earlier versions
            final int childCount = listView.getChildCount();
            if (childCount == 0) {
                return false;
            }

            final int firstPosition = listView.getFirstVisiblePosition();
            if (direction > 0) {
                final int lastBottom = listView.getChildAt(childCount - 1).getBottom();
                final int lastPosition = firstPosition + childCount;
                return lastPosition < listView.getCount()
                        || (lastBottom > listView.getHeight() - listView.getListPaddingBottom());
            } else {
                final int firstTop = listView.getChildAt(0).getTop();
                return firstPosition > 0 || firstTop < listView.getListPaddingTop();
            }
        }
    }
    /**
     * Check if this view can be scrolled vertically in a certain direction.
负数: 顶部是否可以往下滚动
正数: 底部是否可以往上滚动
     *
     * @param direction Negative to check scrolling up, positive to check scrolling down.
     * @return true if this view can be scrolled in the specified direction, false otherwise.
     */
    public boolean canScrollVertically(int direction) {
        final int offset = computeVerticalScrollOffset();
        final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
        if (range == 0) return false;
        if (direction < 0) {
            return offset > 0;
        } else {
            return offset < range - 1;
        }
    }

当SwipeRefreshLayout 拦截了触摸事件之后( mIsBeingDragged 为 true ),将 MotionEvent 交给 onTouchEvent 处理。

@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 = ev.findPointerIndex(mActivePointerId);

            // ... 省略代码

            final float y = ev.getY(pointerIndex);
            startDragging(y);
//计算拖动开始 的位置: mInitialMotionY,变更mIsBeingDragged(是否正在拖动的状态)标志位
/*
    private void startDragging(float y) {
        final float yDiff = y - mInitialDownY;
        if (yDiff > mTouchSlop && !mIsBeingDragged) {
            mInitialMotionY = mInitialDownY + mTouchSlop;
            mIsBeingDragged = true;
            mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
        }
    }
*/
                //移动 圈圈
                if (mIsBeingDragged) {
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    if (overscrollTop > 0) {
                        moveSpinner(overscrollTop);
                    } else {
                        return false;
                    }
                }
            break;
        }//action move end

          //处理多点 触控,变更mActivePointerId
            case MotionEvent.ACTION_POINTER_DOWN: {
                pointerIndex = ev.getActionIndex();
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG,
                            "Got ACTION_POINTER_DOWN event but have an invalid action index.");
                    return false;
                }
                mActivePointerId = ev.getPointerId(pointerIndex);
                break;
            }

//多点触控 手指 数量 变更,如果mActivePointer抬起,更新mActivePointerId,同时调整位置
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                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);
/*
finshSpinner(overScrollTop) 
通过滑动距离,决定是刷新 or 取消刷新
*/
                }
                mActivePointerId = INVALID_POINTER;
                return false;
            }//action Up end

        case MotionEvent.ACTION_CANCEL:
                return false;

    return true;
}

在手指滚动过程中通过判断 mIsBeingDragged 来移动刷新的圆圈(对应的是 moveSpinner ),手指松开将圆圈移动到正确位置(初始位置或者刷新动画的位置,对应的是 finishSpinner 方法)。

// 手指下拉过程中触发的圆圈的变化过程,透明度变化,渐渐出现箭头,大小的变化

private void moveSpinner(float overscrollTop) {

    // 设置为有箭头的 progress
   mProgress.setArrowEnabled(true);

    // 进度转化成百分比
    float originalDragPercent = overscrollTop / mTotalDragDistance;

    // 避免百分比超过 100%
    float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
    // 调整拖动百分比,造成视差效果
    float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
    //
    float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;

    // 这里mUsingCustomStart 为 true 代表用户自定义了起始出现的坐标
    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;

    // 因为有弹性系数,不同的手指滚动距离不同于view的移动距离
    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();
        }
    }
    // 出现的进度,裁剪 mProgress
    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(float overscrollTop)的代码:
通过滑动距离,决定是刷新 or 取消刷新

    private void finishSpinner(float overscrollTop) {
        if (overscrollTop > mTotalDragDistance) {//触发刷新
            //移动距离超过了刷新的临界值,触发刷新动画
            setRefreshing(true, true /* notify */);
/*
//变更圈圈位置,开始刷新

    private void setRefreshing(boolean refreshing, final boolean notify) {
//如果之前刷新状态,与传入刷新状态不一致,则触发下面逻辑
        if (mRefreshing != refreshing) {
            mNotify = notify;
            ensureTarget();
            mRefreshing = refreshing;
            if (mRefreshing) {
                animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
            } else {
                startScaleDownAnimation(mRefreshListener);
            }
        }
    }
*/
        } else {//取消刷新
            // cancel refresh
            mRefreshing = false;
            mProgress.setStartEndTrim(0f, 0f);
            Animation.AnimationListener listener = null;
            if (!mScale) {
                listener = new Animation.AnimationListener() {
                    ...
                    @Override
                    public void onAnimationEnd(Animation animation) {
                        if (!mScale) {
                            startScaleDownAnimation(null);
                        }
                    }
                    ...
                };
            }
            
      //恢复圈圈到初始位置
      animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
//设置没有箭头
mProgress.setArrowEnabled(false);

/*
    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);
        }
    }

*/
        }//取消刷新 end
    }

setRefreshing(...) 说明

可以看到调用 setRefresh(true,true) 方法触发刷新动画并进行回调,但是这个方法是 private 的。前面提到我们自己调用 setRefresh(true) 只能产生动画,而不能回调刷新函数,那么我们就可以用反射调用 2 个参数的 setRefresh 函数。 或者手动调 setRefreshing(true)+ OnRefreshListener.onRefresh 方法。

    private void setRefreshing(boolean refreshing, final boolean notify) {
        if (mRefreshing != refreshing) {//如果之前刷新状态,与传入刷新状态不一致,则触发下面逻辑
            mNotify = notify;
            ensureTarget();
            mRefreshing = refreshing;
            if (mRefreshing) {//开始刷新
                animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
            } else {//取消刷新
            //缩小圈圈    
            startScaleDownAnimation(mRefreshListener);
            }
        }
    }

一个参数的setOnRefreshing(boolean)

    /**
     * Notify the widget that refresh state has changed. Do not call this when
     * refresh is triggered by a swipe gesture.
     * 通知空间:刷新状态已经更改
     * 不要再手势触发刷新时,调用此方法
     *
     * @param refreshing Whether or not the view should show refresh progress.
     * 是否要显示 刷新的progress
     */
    public void setRefreshing(boolean refreshing) {
        if (refreshing && mRefreshing != refreshing) {
            // scale and show
            mRefreshing = refreshing;
            int endTarget = 0;
            if (!mUsingCustomStart) {
                endTarget = mSpinnerOffsetEnd + mOriginalOffsetTop;
            } else {
                endTarget = mSpinnerOffsetEnd;
            }
            setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop);
            mNotify = false;
            startScaleUpAnimation(mRefreshListener);
        } else {
            setRefreshing(refreshing, false /* notify */);
        }
    }

关注上面的mNotify = false,这个标志位,决定了在mRefreshListener(动画回调)中,是否通知SwipeRefreshListenr

 if (mNotify) {
                    if (mListener != null) {
                        mListener.onRefresh();
                    }
                }

这里是false,因此单纯的调用setRefresh(boolean refreshing),只能产生动画,而不能回调刷新函数,这一点,要切记切记

animateOffsetToCorrectPosition

开启一个动画,然后在动画结束后回调 onRefresh 方法。,其中的AnimatoionListener 就是mRefreshingListern,在动画结束时(onAnimationEnd) 调用了mListener.onRefresh

    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);
    }

mRefreshListener

  private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
        //...
        @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();
            }
        }
    };

总结:

分析 SwipeRefreshLayout 的流程就是按照平时我们自定义 ViewGroup 的流程,但是其中也有好多需要我们借鉴的地方,如何使用 NestedScrolling相关机制 ,多点触控的处理,onMeasure 中减去了 padding,如何判断子 View 是否可滚动,如何确定 ViewGroup 中某一个 View 的索引等。 此外,一个好的下拉刷新框架不仅仅要兼容各种滚动的子控件,还要考虑自己要兼容 NestedScrollingChild 的情况,比如放到 CooCoordinatorLayout 的情况,目前大多数开源的下拉刷新好像都没有达到这个要求,一般都是只考虑了内部嵌套滚动子视图的情况,没有考虑自己作为滚动子视图的情况。

相关文章

网友评论

      本文标题:SwipeRefreshLayout - 源码解析

      本文链接:https://www.haomeiwen.com/subject/xbaxdftx.html