Android下拉重新整理控制項SwipeRefreshLayout源碼淺析

來源:互聯網
上載者:User

標籤:

SwipeRefreshLayout是Android官方的下拉重新整理控制項,使用簡單,介面美觀,不熟悉的朋友可以隨便搜尋瞭解一下,這裡就不廢話了,直接進入正題。

這種下拉重新整理控制項的原理不難,基本就是監聽手指的運動,擷取手指的座標,通過計算判斷出是哪種操作,然後就是回調相應的介面了。SwipeRefreshLayout是繼承自ViewGroup的,根據Android的事件分發機制,觸摸事件應該是先傳遞到ViewGroup,根據onInterceptTouchEvent的返回值決定是否攔截事件的,那麼就onInterceptTouchEvent出發:

@Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        ensureTarget();        final int action = MotionEventCompat.getActionMasked(ev);        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {            mReturningToStart = false;        }        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;                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;        }        return mIsBeingDragged;    }

是否攔截的情況有很多種,這裡如果滿足五個條件之一就直接返回false,使用時觸摸事件發生衝突的話就可以從這裡出發分析,這裡也不具體展開了。簡單看一下,在ACTION_DOWN中記錄下手指座標,ACTION_MOVE中計算出移動的距離,並且判斷是否大於閾值,是的話就將mIsBeingDragged標誌位設為true,ACTION_UP中則將mIsBeingDragged設為false。最後返回的是mIsBeingDragged。

SwipeRefreshLayout一般是嵌套可滾動的View使用的,正常滾動時會滿足前面的條件,這時不進行攔截,只有當滾動到頂部才會進入後面action的判斷。在手指按下和抬起期間mIsBeingDragged為true,也就是說進行攔截,接下來就是如何處理了,看看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 = MotionEventCompat.findPointerIndex(ev, mActivePointerId);                if (pointerIndex < 0) {                    Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");                    return false;                }                final float y = MotionEventCompat.getY(ev, pointerIndex);                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;                if (mIsBeingDragged) {                    if (overscrollTop > 0) {                        moveSpinner(overscrollTop);                    } else {                        return false;                    }                }                break;            }           ....            case 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;    }

這裡省略了一些代碼,前面還有幾行跟上面的類似,也是在滿足其中一個條件時直接返回;switch中也還有幾行處理多指觸控的,這些都略過了。看一下ACTION_MOVE中計算了手指移動的距離,這時的mIsBeingDragged正常情況下應為true,當距離大於零就會執行moveSpinner。在ACTION_UP中則會執行finishSpinner,到這裡就可以猜出,執行重新整理的邏輯主要就在這兩個方法中。

看這兩個方法前,要知道兩個重要的成員變數:一個是mCircleView,是CircleImageView的執行個體,繼承了ImageView,主要繪製進度圈的背景;另一個是mProgress,是MaterialProgressDrawable的執行個體,繼承自Drawable且實現Animatable介面,主要繪製進度圈,SwipeRefreshLayout正是通過調用其方法來繪製動畫。接下來就先看一下moveSpinner:

<span style="font-size:18px;">
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 ? mSpinnerFinalOffset - mOriginalOffsetTop                : mSpinnerFinalOffset;        float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)                / slingshotDist);        float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(                (tensionSlingshotPercent / 4), 2)) * 2f;        float extraMove = (slingshotDist) * tensionPercent * 2;        int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);        // 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 */);    }</span>
showArrow是顯示箭頭,中間那一坨主要也是一些math和設定進度圈的樣式,倒數第二行執行了setProgressRotation,傳入的是經過一堆計算後的rotation,這堆計算主要是最佳化效果,比如在剛開始移動時增長比較快,超過重新整理的距離後就增長比較慢。傳入該方法後,mProgress就根據它來繪製進度圈,因此主要的動畫就應該在這個方法內。最後一行執行setTargetOffsetTopAndBottom,我們來看一下:
<span style="font-size:18px;">private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) {        mCircleView.bringToFront();        mCircleView.offsetTopAndBottom(offset);        mCurrentTargetOffsetTop = mCircleView.getTop();        if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {            invalidate();        }    }</span>

比較簡單,就是調整進度圈的位置並進行記錄。最後來看一下finishSpinner:
<span style="font-size:18px;">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) {                    }                };            }            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);            mProgress.showArrow(false);        }    }</span>

邏輯也很簡單,當移動的距離超過設定值時就執行setRefreshing(true,true),在該方法裡更新一些成員變數的值後會執行animateOffsetToCorrectPosition,由名字就知道是執行動畫將進度圈移動到正確位置的(也就是頭部)。如果移動的距離沒有超過設定值,就會執行animateOffsetToStartPosition。一起看一下animateOffsetToCorrectPosition和animateOffsetToStartPosition這兩個方法:

<span style="font-size:18px;">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);    }    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);        }    }</span>
邏輯基本相同,進行一些設定後,最後都會執行mCircleView的startAnimation,只是傳入的值以及監聽器不同。

如果是要執行重新整理的操作,傳入的值是頭部高度,監聽器為:

<span style="font-size:18px;">private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {        @Override        public void onAnimationStart(Animation animation) {        }        @Override        public void onAnimationRepeat(Animation animation) {        }        @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();            }        }    };</span>
動畫完成後,也就是進度圈移動到頭部後,會執行mProgress.start();這裡執行的就是在重新整理時進度圈轉啊轉的動畫。接下來注意到如果mListener不為空白就會執行onRefresh方法,這個mListener其實就是執行setOnRefreshListener所設定的監聽器,因此在這裡完成重新整理。如果是執行回到初始位置的操作,傳入的值為初始高度(也就是頂部之上),監聽器為
<span style="font-size:18px;">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) {    }};</span>
移動到初始位置後會執行startScaleDownAnimation,也就是消失的動畫了,到這裡整個重新整理流程就結束了。
這樣就基本把SwipeRefreshLayout的流程過了一遍,但是要實現這樣一個控制項還是有很多小問題需要考慮的,這裡主要是把思路理清,知道如果出現問題該怎樣解決。另外從源碼也可以看出swipeRefreshLayout的定製性是比較差的,也不知道google是不是故意這樣希望以後全都用這種統一樣式的下拉重新整理。。當然有一些第三方下拉重新整理的定製性還是比較好的,使用上也不難。但是有些人(比如我)是比較傾向於使用官方的控制項的,不到萬不得已都不想用第三方工具。下次會寫一篇探討一下用swipeRefreshLayout實現自訂樣式的文章~

Android下拉重新整理控制項SwipeRefreshLayout源碼淺析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.