1. 程式人生 > >解讀Google官方SwipeRefreshLayout控制元件原始碼,帶你揭祕Android下拉重新整理的實現原理

解讀Google官方SwipeRefreshLayout控制元件原始碼,帶你揭祕Android下拉重新整理的實現原理

前言

想必大家也發現,時下的很多App都應用了這個Google出品的SwipeRefreshLayout下拉重新整理控制元件,它以Material Design風格、適用場景廣泛,簡單易用等特性而獨步江湖。但在我們使用的過程中,不可避免地會發現一些bug,或者需要新增某些特性來滿足需求。出現這些問題,最好的方法就是解讀原始碼,理解它實現的原理,並且在理解原始碼的基礎上修改原始碼,達成需求。然而不知為何,至今還沒有一篇關於SwipeRefreshLayout原始碼解析的文章,所以萌發了要寫一篇這樣的文章。鑑於閱讀技術博文的枯燥,加之還是篇原始碼解析的文章,我不打算一下子扔出來一大段程式碼讓讀者去啃,而是一步一步往下走,揭開SwipeRefreshLayout的神祕面紗。

閱讀原始碼的小技巧

為什麼原始碼普遍都很難讀,有人甚至談之色變?其實程式碼(出自大神之手)生來是易讀的,但程式碼多了就變得難讀了。所以閱讀原始碼時,要把握住主幹,細枝末節可以暫時忽略,一路下來理解了程式工作流程後再回過頭來會有一種豁然開朗的感覺。
閱讀原始碼我還是選擇Android Studio。這個強大的工具提供了很多快捷鍵,大大地方便了原始碼的閱讀。

  • Ctrl+F :在當頁查詢關鍵字
  • Alt+F7: 檢視方法或變數在哪裡被使用過
  • Ctrl+Q:檢視java doc,如果該方法或變數有的話javadoc的話就可以更快知道該它的相關資訊
  • Ctrl+左擊:這個不用說了吧,進入方法體或者檢視定義或者檢視被使用的地方
  • Ctrl+Shift+i:可以不離開當前閱讀的位置,檢視指定方法的方法體
  • Ctrl+F11:加BookMark,簡直是非常有用的功能,不過需要去設定新增一下跳轉下一個書籤或上一個書籤的快捷鍵才能發揮出該功能真正強大。
  • Ctrl+F12 : 輸入關鍵字快速定位指定的變數或方法,支援模糊搜尋。
  • Ctrl +Alt+左箭頭或右箭頭:返回前一個或下一個游標的位置,在想回溯閱讀位置的時候非常有用
  • 關於閱讀原始碼的快捷鍵就這些吧,以後想到了再補充…

你應該知道:

在看往下看之前,我希望你瞭解:

  • 事件分發機制
  • ViewGroup的測量繪製過程

準備工作

所幸該控制元件沒有跟系統api耦合,所以可以直接copy一份程式碼到自己的demo工程中,盡情地改。但是hint會理解報出一些錯誤。首先包名要改一下,類名最好也改吧,以免混淆~其次把CircleImageView和MaterialProgressDrawable這兩個類都copy過來,放在同一個包裡。如圖:
這裡寫圖片描述


如果嫌麻煩可以直接fork我的專案

探究之旅

我們朝著未知的黑暗出發。開啟SwipeRefreshTestLayout的類檔案,看到左邊這麼小的滑塊,其實我一開始是拒絕的~ 感覺無從下手啊有沒有… 沉下心來,想想看看它是繼承於ViewGroup的,所以想想它一定有兩個很關鍵的方法:onMeasure和onLayout,分別解決了它和它的子View佔多大地和擱到哪。因為它是一個下拉重新整理控制元件,它必定要涉及到事件分發的處理,同樣是兩個關鍵方法:onInterceptTouchEvent和onTouchEvent,分別用於決定是否攔截點選事件和進行點選事件的處理。天空瞬間亮了許多…

onMeasure

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
        //mTarget的尺寸為match_parent,除去內邊距
        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));
        //如果mOriginalOffsetTop未被初始化並且mUsingCustomStart ?,則將下拉小圓的初始位置設定成預設值
        if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
            mOriginalOffsetCalculated = true;
            mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
        }
        mCircleViewIndex = -1;
        // Get the index of the circleview.獲取circleview的索引值,主要是為了後面過載getChildDrawingOrder時要用
        for (int index = 0; index < getChildCount(); index++) {
            if (getChildAt(index) == mCircleView) {
                mCircleViewIndex = index;
                break;
            }
        }
    }

我們看到,這個方法程式碼不長,但卻很關鍵。重寫該方法的作用是設定子View的尺寸。出現mTarget是什麼未知生物?其實就是一個它包裹的子View,通常是ListView等一些可滾動的控制元件。ensureTarget();保證它非空並存在。如果不小心包裹了多個VIew呢?則mTarget就是其中的最後一個子View。mCircleView又是什麼生物呢?顧名思義,下拉的白色小圓圈,一個ImageView而已。mCurrentTargetOffsetTop 和mOriginalOffsetTop 是兩個非常關鍵的變數,分別表示當前mCircleView的位置(top值)和初始時mCircleView的位置(top值),當然它們初始化都等於mCircleView高度的負數。還有一個mUsingCustomStart 是什麼呢?我當時也不知道。沒關係,Ctrl+F11打個書籤,等讀完再回頭看。或者我們可以通過Alt+F7看看它的在哪裡被引用過。
這裡寫圖片描述
可以看到,它在setProgressViewOffset被賦值為true,而該方法是用於設定CircleView初始的位置和重新整理停留的位置,Custom是自定義的意思,所以mUsingCustomStart就是一個標誌,表示是否用自定義的起始位置,而預設的起始位置就是CircleView高度的負數。

onLayout

    @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();
        //將mTarget放在覆蓋parent的位置(除去內邊距)
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
        //將mCircleView放在mTarget的平面位置上面居中,初始化時是完全隱藏在螢幕外的
        int circleWidth = mCircleView.getMeasuredWidth();
        int circleHeight = mCircleView.getMeasuredHeight();
        mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
                (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
    }

這個方法程式碼也不長,很簡單,但卻很關鍵。作用是安排子View的位置。將mTarget填充整個控制元件,將mCircleView放在mTarget的平面位置上面居中,初始化時是完全隱藏在螢幕外的。

onInterceptTouchEvent

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

        final int action = MotionEventCompat.getActionMasked(ev);

        //如果當mCircleView正在返回初始位置的同時手指按下了,將標誌mReturningToStart復位
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        //如果下拉被禁用、mCircleView正在返回初始位置、mTarget沒有到達頂部、
        //正在重新整理、mNestedScrollInProgress
        // 不攔截,不處理點選事件,處理權交還mTarget
        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;
                //如果是滑動動作,將mIsBeingDragged置為true
                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;
        }

        //如果正在被拖拽,攔截該系列的點選事件,並呼叫自己的onTouchEvent()來處理
        return mIsBeingDragged;
    }

這個方法的邏輯非常清晰。當如果下拉被禁用、mCircleView正在返回初始位置、mTarget沒有到達頂部、
或者正在重新整理時, 不攔截,不處理點選事件,處理權交還mTarget。排除以上情況後,還需要進一步判斷。
當手指按下時,記錄按下的座標;在MotionEvent.ACTION_MOVE當中,判斷是否是滑動動作,如果是,攔截該系列的點選事件,並呼叫自己的onTouchEvent()來處理。

onTouchEvent

重頭戲來了!這個方法是關鍵中的關鍵:

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        int pointerIndex = -1;

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        //如果被禁用、CircleView正在復位、沒到達頂部、mNestedScrollInProgress,直接返回,不處理該事件
        if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            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) {
                        //spinner可理解為下拉元件,將spinner移到指定的高度
                        //很關鍵的方法,進入看看
                        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;
    }

在case MotionEvent.ACTION_MOVE當中,計算下拉的總高度overscrollTop,DRAG_RATE是下拉阻尼,可以通過改變它的值來改變下拉手感哦~~然後進入到moveSpinner()方法,將spinner移到指定的高度。那麼spinner是啥?其實就是下拉元件的意思。

  • moveSpinner
    /**
     * 通過呼叫setTargetOffsetTopAndBottom()方法移動下拉元件Spinner(mCircleView)
     * 同時更新mProgress(一個drawable)的繪製進度
     * @param overscrollTop 下拉高度
     */
    private void moveSpinner(float overscrollTop) {
        mProgress.showArrow(true);//顯示Progressbar的箭頭

        //經過一系列的計算,spinner控制下拉的最大距離
        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;

        //計算spinner將要(target)被移動到的位置對應的Y座標,當targetY為0時,小圓圈剛好全部露出來
        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-else主要是在通過下拉進度,對mProgress在下拉過程設定顏色透明度,箭頭旋轉角度,縮放大小的控制
        //如果下拉高度小於mTotalDragDistance(一個觸發下拉重新整理的高度)
        if (overscrollTop < mTotalDragDistance) {
            //如果支援下拉小圓圈縮放,設定顏色透明度和縮放大小
            if (mScale) {
                setAnimationProgress(overscrollTop / mTotalDragDistance);
            }
            if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA
                    && !isAnimationRunning(mAlphaStartAnimation)) {
                // Animate the alpha
                startProgressAlphaStartAnimation();
            }
            float strokeStart = adjustedPercent * .8f;
            mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
            mProgress.setArrowScale(Math.min(1f, adjustedPercent));
        } else {//下拉高度達到了觸發重新整理的高度
            if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {
                // Animate the alpha
                startProgressAlphaMaxAnimation();
            }

            ViewCompat.setScaleX(mCircleView, 1f);
            ViewCompat.setScaleY(mCircleView, 1f);
        }
        float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
        mProgress.setProgressRotation(rotation);

        //這是關鍵呼叫!動態設定mSpinner的高度。進入該函式看看
        setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
    }

該方法主要乾的事就是通過呼叫setTargetOffsetTopAndBottom()方法移動下拉元件Spinner(mCircleView),同時更新mProgress(一個drawable)的繪製進度。其中進行了一些複雜的運算,其實就是在控制下拉的最大高度,避免使用者無限下拉…說明一下,這個mScale,因為我已經添加了javadoc,讀者Ctrl+Q就可以檢視它的相關資訊。它覺得spinner下拉過程是否支援縮放,可以通過setProgressViewEndTarget()和setProgressViewOffset()設定。但我發現一個bug,如果手指下拉過快,小圓就會來不及放到最大…小圓明顯變小了,如圖
這裡寫圖片描述
好,有改bug的希望就有了繼續閱讀的動力!我們進入setTargetOffsetTopAndBottom()看看。

  • setTargetOffsetAndBottom
    /**
     * 設定mCircleView的偏移量
     * 同時更新mCurrentTargetOffsetTop
     * @param offset 偏移量,可正可負
     * @param requiresUpdate 介面是否需要重繪invalidate();
     */
    private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) {
        //bringToFront()該方法會呼叫requestLayout()和invalidate()把view放到前面
        //已經重寫了getChildDrawingOrder方法,所以沒有必要再呼叫該方法了,我個人認為...
        //可通過手動呼叫invalidate()來代替它
//        mCircleView.bringToFront();
        mCircleView.offsetTopAndBottom(offset);
        mCurrentTargetOffsetTop = mCircleView.getTop();
        if (requiresUpdate /*&& android.os.Build.VERSION.SDK_INT < 11*/) {
            invalidate();
        }
    }

該方法很短,卻是mCircleView能夠下拉的精髓所在啊!offsetTopAndBottom()本質上是呼叫layout(getLeft(),getTop()+offsetY,getRight(),getBottom()+offsetY);(注意不是onLayout())同時對mCircleView的top和bottom進行偏移,offset是View整體在垂直方向上的偏移量。這裡我把bringToFront()註釋掉,bringToFront()該方法會呼叫requestLayout()和invalidate()把view放到前面,因為已經重寫了getChildDrawingOrder方法,所以沒有必要再呼叫該方法了,我個人認為…可通過手動呼叫invalidate()來代替它。

到此,我們已經瞭解過它下拉的的過程,下面進行回溯到onTouchEvent的case MotionEvent.ACTION_UP:

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

計算完了鬆開手時下拉的總距離後,交給方法finishSpinner(overscrollTop);處理。進去看看。

  • finishSpinner(overscrollTop)
    /**
     * 手指鬆開後,處理下拉元件Spinner
     * 設定開始重新整理的動畫,或者
     * 將下拉元件Spinner回滾隱藏
     * @param overscrollTop 下拉距離
     */
    private void finishSpinner(float overscrollTop) {
        if (overscrollTop > mTotalDragDistance) {//下拉距離達到了可觸發重新整理的高度
            //關鍵方法
            setRefreshing(true, true /* notify */);
        } else {//下拉距離還未達到了可觸發重新整理的高度,做一些復位的操作
            // cancel refresh
            mRefreshing = false;
            mProgress.setStartEndTrim(0f, 0f);
            //值得關注的是這個回滾動畫
            AnimationListener listener = null;
            if (!mScale) {
                listener = new AnimationListener() {
                    @Override
                    public void onAnimationEnd(Animation animation) {
                        if (!mScale) {
                            startScaleDownAnimation(null);
                        }
                    }
                };
            }
            //開始回滾動畫
            //這是一個比較複雜的方法,也是比較有用的方法
            //其實這個本質上不是開啟一個動畫,而是一個數值產生器
            //通過監聽數值變化,
            //從mCurrentTargetOffsetTop這個高度開始,
            //呼叫setTargetOffsetTopAndBottom()慢慢回滾到mOriginalOffsetTop
            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
            mProgress.showArrow(false);
        }
    }

這裡看到的mTotalDragDistance同樣可以通過Ctrl+Q檢視它的資訊。這個方法只有一對大大的if-else,如果下拉距離達到了可觸發重新整理的高度,開始重新整理;否則開始回滾動畫,將Spinner回滾到開始的位置(也就是mOriginalOffsetTop)。而animateOffsetToStartPosition這個方法是一個內涵很豐富的方法,涉及到多步跳轉才能瞭解徹底。大家可以去github fork下來,找到相應方法Ctrl+左擊進去看看,裡面的方法都添加了詳細的註釋,相信大家一定能看懂。有朋友可能會問,這裡怎麼用檢視動畫而不用屬性動畫呢?其實這裡並不是開啟一個真正意義上的動畫,而是一個數值產生器,通過監聽數值變化,從mCurrentTargetOffsetTop這個高度開始,呼叫setTargetOffsetTopAndBottom()慢慢回滾到mOriginalOffsetTop。
下面我們一起來看看setRefreshing(true, true /* notify */);

  • setRefreshing(true, true /* notify */)
    /**
     * 設定重新整理狀態,該方法通常不是由類庫使用者來呼叫,而是在使用者下拉的時候由SwipeRefreshLayout來呼叫
     * @param refreshing 是否重新整理
     * @param notify 是否回撥onRefresh()
     */
    private void setRefreshing(boolean refreshing, final boolean notify) {
        if (mRefreshing != refreshing) {
            mNotify = notify;
            ensureTarget();
            mRefreshing = refreshing;
            if (mRefreshing) {//啟動重新整理
                animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
            } else {//停止重新整理
                //開始Spinner消失動畫
                startScaleDownAnimation(mRefreshListener);
            }
        }
    }

該方法通常不是由類庫使用者來呼叫,而是在使用者下拉的時候由SwipeRefreshLayout自己來呼叫,所以它是private的。如果啟動重新整理,則呼叫animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);將mCircleView移到正確的高度(也就是mSpinnerFinalOffset),animateOffsetToCorrectPosition()跟上文提到的animateOffsetToStartPosition()方法的實現機理是完全一樣的。我們這裡回想一下,剛才的bug是由於手指鬆開時mCircleView的Scale值沒有達到1,那麼在這裡我們就可以在它的移動到重新整理位置的動畫結束時,把它的Scale手動設定為1。

    private AnimationListener mRefreshListener = new AnimationListener() {
        @Override
        public void onAnimationEnd(Animation animation) {
            if (mRefreshing) {
                //改bug
                ViewCompat.setScaleX(mCircleView, 1f);
                ViewCompat.setScaleY(mCircleView, 1f);
                // Make sure the progress view is fully visible
                mProgress.setAlpha(MAX_ALPHA);
                mProgress.start();
                if (mNotify) {
                    if (mListener != null) {
                        mListener.onRefresh();
                    }
                }
            } else {
                mProgress.stop();
                mCircleView.setVisibility(View.GONE);
                setColorViewAlpha(MAX_ALPHA);
                // Return the circle to its start position
                if (mScale) {
                    setAnimationProgress(0 /* animation complete and view is hidden */);
                } else {
                    setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop,
                            true /* requires update */);
                }
            }
            mCurrentTargetOffsetTop = mCircleView.getTop();
        }
    };

效果還可吧!
這裡寫圖片描述
我們發現onRefresh()是在這個被回撥的,而且僅在這裡被回撥。

不知不覺,天亮了~框架脈絡已經很清晰了吧。
還有一些變數或方法的名字帶有NestedScroll沒有提到,其實那是跟巢狀滑動有關的,不知道也不影響原始碼的閱讀。

下面說說我遇到過的一個問題,當我們在Activity的onCreate中

        mRefreshLayout = (SwipeRefreshTestLayout) findViewById(R.id.refresh_widget);
        mRefreshLayout.setRefreshing(true);
  • 延遲呼叫
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mRefreshLayout.setRefreshing(true);
            }
        },100);

或者

        mRefreshLayout.postDelayed(new Runnable() {
            @Override
            public void run() {
                mRefreshLayout.setRefreshing(false);
            }
        }, 3000);
  • 改在onWindowFocusChanged當中呼叫
    因為回撥該函式時Activity處於可見狀態,注意如果在onResume中呼叫還是會沒有效果的。
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        mRefreshLayout.setRefreshing(true);
    }
  • 既然我們已經是看過原始碼的人了,能不能在setRefreshing的原始碼中解決這個問題呢?
    這是我自己的腦洞方案:
    public void setRefreshing(final boolean refreshing) {
        //防止類庫使用者在SwipeRefreshLayout還沒完全被初始化時呼叫該方法
        //還是建議使用者重寫Activity的onWindowFocusChanged()方法來呼叫setRefreshing(true);
        if (!isShown()&& refreshing){
            Log.e("SwipeRefreshLayout", "It's not advisable to invoke setRefreshing() when SwipeRefreshLayout is inVisible.");
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    setRefreshing(true);
                }
            },50/*該時間可以任意設定*/);
            return;
        }
        .....省略若干程式碼......
     }

專案程式碼已上傳至Github。—repo

點star和轉發也是一種支援!

如果你發現有什麼不清楚或不妥的地方歡迎留言討論。