SwipeRefreshLayout 源碼分析

## SwipeRefreshLayout 源碼分析

> 本文基於 v4 版本 `23.2.0`

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 的步驟:

1. 初始化變量
2. onMeasure
3. onLayout
4. 處理交互 (`dispatchTouchEvent` `onInterceptTouchEvent` `onTouchEvent`)

接下來就按照上面的步驟進行分析。



#### 1.初始化變量


`SwipeRefreshLayout` 內部有 2 個 View,一個`圓圈(mCircleView)`,一個內部可滾動的` View(mTarget)`。除了 View,還包含一個 `OnRefreshListener` 接口,當刷新動畫被觸發時回調。


 ![圖片](https://dn-coding-net-production-pp.qbox.me/8e02212d-b364-4df8-bfaa-47f3084f89e7.png)


```java
/**
 * 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();

    // 系統默認的動畫時長
    mMediumAnimationDuration = getResources().getInteger(
            android.R.integer.config_mediumAnimTime);

    setWillNotDraw(false);
    mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);

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

    // 刷新的圓圈的大小,單位轉換成 sp
    final DisplayMetrics metrics = getResources().getDisplayMetrics();
    mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density);
    mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density);

    // 建立刷新動畫的圓圈
    createProgressView();

    ViewCompat.setChildrenDrawingOrderEnabled(this, true);
    // the absolute offset has to take into account that the circle starts at an offset
    mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density;
    // 刷新動畫的臨界距離值
    mTotalDragDistance = mSpinnerFinalOffset;

    // 經過 NestedScrolling 機制來處理嵌套滾動
    mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
    setNestedScrollingEnabled(true);
}
```

// 建立刷新動畫的圓圈
```java
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);
}
```

初始化的時候建立一個出來一個 View (下拉刷新的圓圈)。能夠看出使用背景圓圈是 v4 包裏提供的 `CircleImageView` 控件,中間的是 `MaterialProgressDrawable` 進度條。
另外一個 View 是在 xml 中包含的可滾動視圖。

#### 2.onMeasure

onMeasure 肯定子視圖的大小。

```java
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (mTarget == null) {
        // 肯定內部要滾動的View,如 RecycleView
        ensureTarget();
    }
    if (mTarget == null) {
        return;
    }

    // 測量子 View (mTarget)
    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));

    if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
        mOriginalOffsetCalculated = true;
        mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
    }

    // 計算 mCircleView 在 ViewGroup 中的索引
    mCircleViewIndex = -1;
    // Get the index of the circleview.
    for (int index = 0; index < getChildCount(); index++) {
        if (getChildAt(index) == mCircleView) {
            mCircleViewIndex = index;
            break;
        }
    }
}
```

這個步驟肯定了 mCircleView 和 SwipeRefreshLayout 的子視圖的大小。


#### 3.onLayout

onLayout 主要負責肯定各個子視圖的位置。

```java
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
   // 獲取 SwipeRefreshLayout 的寬高
   final int width = getMeasuredWidth();
   final int height = getMeasuredHeight();
   if (getChildCount() == 0) {
       return;
   }
   if (mTarget == null) {
       ensureTarget();
   }
   if (mTarget == null) {
       return;
   }
   // 考慮到給控件設置 padding,去除 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();
   // 設置 mTarget 的位置
   child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
   int circleWidth = mCircleView.getMeasuredWidth();
   int circleHeight = mCircleView.getMeasuredHeight();
   // 根據 mCurrentTargetOffsetTop 變量的值來設置 mCircleView 的位置
   mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
           (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
}
```
 ![圖片](https://dn-coding-net-production-pp.qbox.me/8df6d458-700b-4ec5-b731-c6b8c34cdddc.png)

在 onLayout 中放置了 mCircleView 的位置,注意 頂部位置是 mCurrentTargetOffsetTop ,mCurrentTargetOffsetTop 初始距離是`-mCircleView.getMeasuredHeight()`,因此是在 SwipeRefreshLayout 外。


> 通過以上幾個步驟,SwipeRefreshLayout 建立了子視圖,肯定他們的大小、位置,如今全部視圖能夠顯示在界面了。

### 處理與子視圖的滾動交互

下拉刷新控件的主要功能是當子視圖下拉到最頂部時,繼續下拉能夠出現刷新動畫。而子視圖能夠滾動時須要將全部滾動事件都交給子視圖。藉助 Android 提供的 NestedScrolling 機制,使得 SwipeRefreshLayout 很輕鬆的解決了與子視圖的滾動衝突問題。
SwipeRefreshLayout 經過實現 `NestedScrollingParent` 和 `NestedScrollingChild` 接口來處理滾動衝突。SwipeRefreshLayout 做爲 Parent 嵌套一個能夠滾動的子視圖,那麼就須要瞭解一下 NestedScrollingParent 接口


```java
/**
 當你但願本身的自定義佈局支持嵌套子視圖而且處理滾動操做,就能夠實現該接口。
 實現這個接口後能夠建立一個 NestedScrollingParentHelper 字段,使用它來幫助你處理大部分的方法。
 處理嵌套的滾動時應該使用  `ViewCompat`,`ViewGroupCompat`或`ViewParentCompat` 中的方法來處理,這是一些兼容庫,
 他們保證 Android 5.0以前的兼容性墊片的靜態方法,這樣能夠兼容 Android 5.0 以前的版本。
 */
public interface NestedScrollingParent {
    /**
     * 當子視圖調用 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 表示響應子視圖的滾動。
     */
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    /**
     * 若是 onStartNestedScroll 返回 true ,而後走該方法,這個方法裏能夠作一些初始化。
     */
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);


    /**
     * 子視圖開始滾動前會調用這個方法。這時候父佈局(也就是當前的 NestedScrollingParent 的實現類)能夠經過這個方法來配合子視圖同時處理滾動事件。
     *
     * @param target 滾動的子視圖
     * @param dx 絕對值爲手指在x方向滾動的距離,dx<0 表示手指在屏幕向右滾動
     * @param dy 絕對值爲手指在y方向滾動的距離,dy<0 表示手指在屏幕向下滾動
     * @param consumed 一個數組,值用來表示父佈局消耗了多少距離,未消耗前爲[0,0], 若是父佈局想處理滾動事件,就能夠在這個方法的實現中爲consumed[0],consumed[1]賦值。
     *                 分別表示x和y方向消耗的距離。如父佈局想在豎直方向(y)徹底攔截子視圖,那麼讓 consumed[1] = dy,就把手指產生的觸摸事件給攔截了,子視圖便響應不到觸摸事件了 。
     */
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);


  /**
     * 這個方法表示子視圖正在滾動,而且把滾動距離回調用到該方法,前提是 onStartNestedScroll 返回了 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>
     *
     * @param target 滾動的子視圖
     * @param dxConsumed 手指產生的觸摸距離中,子視圖消耗的x方向的距離
     * @param dyConsumed 手指產生的觸摸距離中,子視圖消耗的y方向的距離 ,若是 onNestedPreScroll 中 dy = 20, consumed[0] = 8,那麼 dy = 12
      * @param dxUnconsumed 手指產生的觸摸距離中,未被子視圖消耗的x方向的距離
     * @param dyUnconsumed 手指產生的觸摸距離中,未被子視圖消耗的y方向的距離
     */
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed);



    /**
     * 響應嵌套滾動結束
     *
     * 當一個嵌套滾動結束後(如MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL)會調用該方法,在這裏可有作一些收尾工做,好比變量重置
     */
    public void onStopNestedScroll(View target);


    /**
     * 手指在屏幕快速滑觸發Fling前回調,若是前面 onNestedPreScroll 中父佈局消耗了事件,那麼這個也會被觸發
     * 返回true表示父佈局徹底處理 fling 事件
     *
     * @param target 滾動的子視圖
     * @param velocityX x方向的速度(px/s)
     * @param velocityY y方向的速度
     * @return true if this parent consumed the fling ahead of the target view
     */
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    /**
     * 子視圖fling 時回調,父佈局能夠選擇監聽子視圖的 fling。
     * true 表示父佈局處理 fling,false表示父佈局監聽子視圖的fling
     *
     * @param target View that initiated the nested scroll
     * @param velocityX Horizontal velocity in pixels per second
     * @param velocityY Vertical velocity in pixels per second
     * @param consumed true 表示子視圖處理了fling

     */
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    /**
     * 返回當前 NestedScrollingParent 的滾動方向,
     *
     * @return
     * @see ViewCompat#SCROLL_AXIS_HORIZONTAL
     * @see ViewCompat#SCROLL_AXIS_VERTICAL
     * @see ViewCompat#SCROLL_AXIS_NONE
     */
    public int getNestedScrollAxes();
}

```

看一下 SwipeRefreshLayout 實現 NestedScrollingParent 的相關方法
```java
// NestedScrollingParent

// 子 View (NestedScrollingChild)開始滾動前回調此方法,返回 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);

     // ...省略代碼
 }
```
SwipeRefreshLayout 只接受豎直方向(Y軸)的滾動,而且在刷新動畫進行中不接受滾動。

```java
// NestedScrollingChild 在滾動的時候會觸發, 看父類消耗了多少距離
//   * @param dx x 軸滾動的距離
//   * @param dy y 軸滾動的距離
//   * @param consumed 表明 父 View 消費的滾動距離
//
@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 = 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) {
    // ... 省略代碼

    // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
    // sometimes between two nested scrolling views, we need a way to be able to know when any
    // nested scrolling parent has stopped handling events. We do that by using the
    // 'offset in window 'functionality to see if we have been moved from the event.
    // This is a decent indication of whether we should take over the event stream or not.
    // 手指在屏幕上向下滾動,而且子視圖不能夠滾動
    final int dy = dyUnconsumed + mParentOffsetInWindow[1];
    if (dy < 0 && !canChildScrollUp()) {
        mTotalUnconsumed += Math.abs(dy);
        moveSpinner(mTotalUnconsumed);
    }
}
```
SwipeRefreshLayout 經過 NestedScrollingParent 接口完成了處理子視圖的滾動的衝突,中間省略了一些 SwipeRefreshLayout做爲 child 的相關代碼,這種狀況是爲了兼容將 SwipeRefreshLayout 做爲子視圖放在知識嵌套滾動的父佈局的狀況,這裏不作深刻討論。可是下拉刷新須要判斷手指在屏幕的狀態來進行一個刷新的動畫,因此咱們還須要處理觸摸事件,判斷手指在屏幕中的狀態。


首先是 onInterceptTouchEvent,返回 true 表示攔截觸摸事件。

```java


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureTarget();

    final int action = MotionEventCompat.getActionMasked(ev);

    // 手指按下時恢復狀態
    if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
        mReturningToStart = false;
    }

    // 控件可用 || 刷新事件剛結束正在恢復初始狀態時 || 子 View 可滾動 || 正在刷新 || 父 View 正在滾動
    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;
            }
            // 判斷當拖動距離大於最小距離時設置 mIsBeingDragged = true;
            final float yDiff = y - mInitialDownY;
            if (yDiff > mTouchSlop && !mIsBeingDragged) {
                mInitialMotionY = mInitialDownY + mTouchSlop;
                mIsBeingDragged = true;
                // 正在拖動狀態,更新圓圈的 progressbar 的 alpha 值
                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;
}

```
能夠看到源碼也就是進行簡單處理,DOWN 的時候記錄一下位置,MOVE 時判斷移動的距離,返回值 mIsBeingDragged 爲 true 時, 即 onInterceptTouchEvent 返回true,SwipeRefreshLayout 攔截觸摸事件,不分發給 mTarget,而後把 MotionEvent 傳給 onTouchEvent 方法。其中有一個判斷子View的是否還能夠滾動的方法 `canChildScrollUp`。

```java
/**
 * @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 (android.os.Build.VERSION.SDK_INT < 14) {
        // 判斷 AbsListView 的子類 ListView 或者 GridView 等
        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 {
        return ViewCompat.canScrollVertically(mTarget, -1);
    }
}

```

當SwipeRefreshLayout 攔截了觸摸事件以後( mIsBeingDragged 爲 true ),將 MotionEvent 交給 onTouchEvent 處理。
```java

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

            // ... 省略代碼

            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;
        }
        // ... 省略代碼
    }

    return true;
}

```

在手指滾動過程當中經過判斷 mIsBeingDragged 來移動刷新的圓圈(對應的是 moveSpinner ),手指鬆開將圓圈移動到正確位置(初始位置或者刷新動畫的位置,對應的是 finishSpinner 方法)。

```java
// 手指下拉過程當中觸發的圓圈的變化過程,透明度變化,漸漸出現箭頭,大小的變化
private void moveSpinner(float overscrollTop) {

    // 設置爲有箭頭的 progress
    mProgress.showArrow(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 */);
}
```

刷新圓圈的移動過程也是有好幾種狀態,看上面的註釋基本上就比較清楚了。

```java
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)
    }
}

```

能夠看到調用 setRefresh(true,true) 方法觸發刷新動畫並進行回調,可是這個方法是 private 的。前面提到咱們本身調用 setRefresh(true) 只能產生動畫,而不能回調刷新函數,那麼咱們就能夠用反射調用 2 個參數的 setRefresh 函數。 或者手動調 setRefreshing(true)+ OnRefreshListener.onRefresh 方法。


### setRefresh

```java
/**
  * 改變刷新動畫的的圓圈刷新狀態。Notify the widget that refresh state has changed. Do not call this when
  * refresh is triggered by a swipe gesture.
  *
  * @param refreshing 是否顯示刷新的圓圈
  */
 public void setRefreshing(boolean refreshing) {
     if (refreshing && mRefreshing != refreshing) {
         // scale and show
         mRefreshing = refreshing;
         int endTarget = 0;
         if (!mUsingCustomStart) {
             endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop);
         } else {
             endTarget = (int) mSpinnerFinalOffset;
         }
         setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop,
                 true /* requires update */);
         mNotify = false;
         startScaleUpAnimation(mRefreshListener);
     } else {
         setRefreshing(refreshing, false /* notify */);
     }
 }
```

startScaleUpAnimation 開啓一個動畫,而後在動畫結束後回調 onRefresh 方法。

```java
private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
   // .. 省略代碼
   @Override
   public void onAnimationEnd(Animation animation) {
       if (mRefreshing) {
           mProgress.setAlpha(MAX_ALPHA); //確保刷新圓圈中間的進度條是徹底不透明瞭
           mProgress.start();
           if (mNotify) { // 當 mNotify 爲 true 時纔會回調 onRefresh
               if (mListener != null) {
                   // 回調 listener 的 onRefresh 方法
                   mListener.onRefresh();
               }
           }
           mCurrentTargetOffsetTop = mCircleView.getTop();
       } else {
           reset();
       }
   }
};
```

## 總結

分析 SwipeRefreshLayout 的流程就是按照平時咱們自定義 `ViewGroup` 的流程,可是其中也有好多須要咱們借鑑的地方,如何使用 NestedScrolling相關機制 ,多點觸控的處理,onMeasure 中減去了 padding,如何判斷子 View 是否可滾動,如何肯定 ViewGroup 中某一個 View 的索引等。
此外,一個好的下拉刷新框架不單單要兼容各類滾動的子控件,還要考慮本身要兼容 NestedScrollingChild 的狀況,好比放到 CooCoordinatorLayout 的狀況,目前大多數開源的下拉刷新好像都沒有達到這個要求,通常都是隻考慮了內部嵌套滾動子視圖的狀況,沒有考慮本身做爲滾動子視圖的狀況。
相關文章
相關標籤/搜索