Android—有趣的嵌套滑動(dòng)

我的CSDN: ListerCi
我的簡書: 東方未曦

寫在前面

博客中的demo上傳到了github NestedScrollingProject茂装,歡迎各位同學(xué)下載&star。

一善延、吸頂效果&RecyclerView源碼簡析

吸頂效果是CoordinatorLayout中的一個(gè)基礎(chǔ)功能少态,它的本質(zhì)就是嵌套滑動(dòng),因此我們可以自己嘗試去實(shí)現(xiàn)它易遣。同時(shí)本章將會(huì)對(duì)RecyclerView源碼中的嵌套滑動(dòng)部分進(jìn)行分析彼妻,深入理解嵌套滑動(dòng)事件的分發(fā)與回調(diào)。

1.1 吸頂效果展示

效果展示.gif

1.2 嵌套滑動(dòng)API介紹

上面所展示的界面是一個(gè)線性布局豆茫,如圖所示:

布局文件.png

外部父Layout包裹ImageView侨歉、TextView和RecyclerView,如果我們希望滑動(dòng)RecyclerView的時(shí)候能先將ImageView滑動(dòng)上去揩魂,隨后使TextView吸頂思恐,我們?cè)撛趺醋瞿兀?/p>

這里就用到嵌套滑動(dòng)颅痊,假設(shè)當(dāng)前用戶手指在RecyclerView向上劃動(dòng)垒迂,我們需要將RecyclerView的滑動(dòng)事件先傳遞給父布局堂污,如果父布局發(fā)現(xiàn)頭部的ImageView還在顯示茅特,那么先消耗該事件并將整個(gè)父布局中的所有內(nèi)容向上移動(dòng);如果圖片已經(jīng)上滑至不顯示棋枕,那么將滑動(dòng)事件交給RecyclerView處理。

手指在RecyclerView上劃時(shí)如圖所示妒峦,此時(shí)LinearLayout中的所有內(nèi)容都會(huì)向上滾動(dòng)重斑,直到TextView吸頂,再開始滑動(dòng)RecyclerView肯骇。注意:RecyclerView的高度其實(shí)是界面的高度減去TexView的高度窥浪,比布局文件圖中畫的高度要高。

圖片-移動(dòng)示意.png

根據(jù)上面的流程不難發(fā)現(xiàn)笛丙,嵌套滑動(dòng)由RecyclerView主動(dòng)發(fā)起漾脂,父View被動(dòng)接受,并且父View可以先于子View處理滑動(dòng)事件胚鸯。舉個(gè)栗子骨稿,假設(shè)在一次事件中手指在RecyclerView向上滑動(dòng)dy,那么大體的流程如下:
① RecyclerView判斷是否有父View能接受嵌套滑動(dòng)姜钳,如果有坦冠,則將事件傳遞給父View。
② 父View收到該滑動(dòng)事件哥桥,此時(shí)父View判斷當(dāng)前圖片是否還在展示辙浑,如果還在展示,則父View向上滑動(dòng)拟糕。但是父View不一定會(huì)在每次事件中都將dy全部消耗掉(例如滑動(dòng)到邊緣的時(shí)候)判呕,這里通過一個(gè)值consumed來保存父Layout消耗的值。
③ 子View根據(jù)父View消耗的距離送滞,計(jì)算出剩余的值dy-consumed侠草,如果dy-consumed不為0,則由RecyclerView自己處理犁嗅。
④ 如果RecyclerView消耗完之后剩余的距離還不為0梦抢,則再交由父Layout處理。

想要實(shí)現(xiàn)嵌套滑動(dòng)的子View需要實(shí)現(xiàn)NestedScrollingChild接口愧哟,里面包含的方法如下奥吩。

public interface NestedScrollingChild {
    // 設(shè)置當(dāng)前子View是否支持嵌套滑動(dòng)
    void setNestedScrollingEnabled(boolean enabled);

    // 當(dāng)前子View是否支持嵌套滑動(dòng)
    boolean isNestedScrollingEnabled();

    // 開始嵌套滑動(dòng),對(duì)應(yīng)Parent的onStartNestedScroll
    boolean startNestedScroll(@ScrollAxis int axes);

    // 停止本次嵌套滑動(dòng)蕊梧,對(duì)應(yīng)Parent的onStopNestedScroll
    void stopNestedScroll();

    // true表示這個(gè)子View有一個(gè)支持嵌套滑動(dòng)的父View
    boolean hasNestedScrollingParent();

    // 通知父Layout即將開始滑動(dòng)了霞赫,由父View先處理,對(duì)應(yīng)父View的onNestedPreScroll方法
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow);

    // 子View處理完事件再交給父View肥矢,對(duì)應(yīng)父View的onNestedScroll方法
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);

    // 通知父View開始Fling了端衰,對(duì)應(yīng)Parent的onNestedFling方法
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    // 通知父View要開始fling了叠洗,對(duì)應(yīng)Parent的onNestedPreFling方法
    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

想要實(shí)現(xiàn)嵌套滑動(dòng)的父View需要實(shí)現(xiàn)NestedScrollingParent接口,里面包含的方法如下旅东。

public interface NestedScrollingParent {
    // 當(dāng)子View開始滑動(dòng)時(shí)調(diào)用灭抑,返回true表示接受嵌套滑動(dòng)
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);

    // 接受嵌套滑動(dòng)后進(jìn)行準(zhǔn)備工作
    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);

    // 嵌套滑動(dòng)結(jié)束時(shí)回調(diào)
    void onStopNestedScroll(@NonNull View target);

    // 父View先處理滑動(dòng)距離dx或dy,consumed[0]保存父Layout在x軸上消耗的距離抵代,consumed[1]保存父Layout在y軸上消耗的距離
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);

    // 父View處理子View消耗完后剩余的距離
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    // 當(dāng)子View fling時(shí)腾节,會(huì)觸發(fā)這個(gè)回調(diào),consumed代表速度是否被子View消耗
    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);

    // 當(dāng)子View要開始fling時(shí)荤牍,會(huì)先詢問父View是否要攔截本次fling案腺,返回true表示要攔截,那么子View就不會(huì)慣性滑動(dòng)了
    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);

    // 表示目前正在進(jìn)行的嵌套滑動(dòng)的方向康吵,值有:
    // ViewCompat.SCROLL_AXIS_HORIZONTAL
    // ViewCompat.SCROLL_AXIS_VERTICAL劈榨、SCROLL_AXIS_NONE
    @ScrollAxis
    int getNestedScrollAxes();
}

可以看到這兩個(gè)接口的方法名都很通俗易懂,子View主動(dòng)觸發(fā)嵌套滑動(dòng)晦嵌,父View被動(dòng)接受觸發(fā)回調(diào)同辣,每一個(gè)嵌套滑動(dòng)事件都會(huì)經(jīng)歷一個(gè)"父-子-父"的分發(fā)流程。以RecyclerView為例惭载,一次嵌套滑動(dòng)事件的執(zhí)行順序如下所示:

-> child.ACTION_DOWN
-> child.startNestedScroll
-> parent.onStartNestedScroll (如果返回false邑闺,則流程終止)
-> parent.onNestedScrollAccepted
-> child.ACTION_MOVE
-> child.dispatchNestedPreScroll
-> parent.onNestedPreScroll
-> child.ACTION_UP
-> chid.stopNestedScroll
-> parent.onStopNestedScroll
-> child.fling
-> child.dispatchNestedPreFling
-> parent.onNestedPreFling
-> child.dispatchNestedFling
-> parent.onNestedFling

那么問題來了,子View主動(dòng)開啟嵌套滑動(dòng)之后父View是怎么接收到的呢棕兼?
那就不得不提兩個(gè)工具類NestedScrollingChildHelperNestedScrollingParentHelper了,這兩個(gè)工具類的作用就是連接父View和子View并完成一些基礎(chǔ)工作靶衍。當(dāng)子View調(diào)用startNestedScroll()方法時(shí)颅眶,內(nèi)部究竟做了什么呢涛酗?來看一下RecyclerView里的寫法。

@Override
public boolean startNestedScroll(int axes) {
    return getScrollingChildHelper().startNestedScroll(axes);
}

emmmm...直接調(diào)用了NestedScrollingChildHelperstartNestedScroll(axes)方法,這里的axes表示方向过蹂,點(diǎn)進(jìn)去看下酷勺。

public boolean startNestedScroll(@ScrollAxis int axes) {
    return startNestedScroll(axes, TYPE_TOUCH);
}

這方法是個(gè)套娃坦报,再點(diǎn)進(jìn)去看下片择。

    public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

終于看到方法本體了字管,type參數(shù)表示什么下面再談硫戈,看一下方法做了什么:
mView表示當(dāng)前這個(gè)子View梭姓,方法里一層一層向上尋找mView的父View铡恕,直到ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)返回true,也就是此時(shí)的父View實(shí)現(xiàn)了NestedScrollingParent系列接口并接受此次嵌套滑動(dòng)墙牌〕贩溃看一下ViewParentCompat的onStartNestedScroll(...)方法漾肮。

    public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
        if (parent instanceof NestedScrollingParent2) {
            return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            if (Build.VERSION.SDK_INT >= 21) {
                // ......
            } else if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
        }
        return false;
    }

從這里可以看出來谭溉,嵌套滑動(dòng)的parent不一定是child的直接父View,它們中間可能隔了好幾層旅挤。仔細(xì)看一下上面的方法,你會(huì)發(fā)現(xiàn)除了NestedScrollingParent接口外還有NestedScrollingParent2接口,那么相比于第1代,NestedScrollingParent2升級(jí)了什么呢拐辽?

還記得上面提到的type參數(shù)嗎睁搭?第2代嵌套滑動(dòng)接口通過該參數(shù)區(qū)分當(dāng)前觸發(fā)嵌套滑動(dòng)的是SCROLL事件還是FLING事件锌唾,父View可以統(tǒng)一在onNestedPreScroll()onNestedScroll()方法中進(jìn)行處理。至于這是怎么做到的,我們接著往下看。

1.3 RecyclerView嵌套滑動(dòng)源碼簡析(版本androidx-1.1.0)

現(xiàn)在先讓我們來探究一下嵌套滑動(dòng)的源頭,上面提到,嵌套滑動(dòng)是由子View發(fā)起,父Layout接收的,那么子View究竟在什么時(shí)候開啟嵌套滑動(dòng)呢澄步?RecyclerView在嵌套滑動(dòng)中經(jīng)常作為子View王凑,這里以RecyclerView為例弱睦,來分析其處理嵌套滑動(dòng)的邏輯火惊,該邏輯主要在onTouchEvent()方法中惶岭,來看一下精簡后的代碼。

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        // 省略部分代碼......
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                // 手指按下時(shí)嘗試開啟嵌套滑動(dòng), 尋找可以嵌套滑動(dòng)的父Layout
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;

            case MotionEvent.ACTION_MOVE: {
                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    // 根據(jù)當(dāng)前的滑動(dòng)方向開始嵌套滑動(dòng), 由父Layout先scroll
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH)) {
                        // 減去父Layout消耗掉的距離
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        // 更新offsets, 不常用到
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // 滑動(dòng)已經(jīng)初始化, 阻止父Layout攔截事件
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    // RecyclerView內(nèi)部的scroll
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
            } break;

            case MotionEvent.ACTION_UP: {
                // 手指抬起時(shí)計(jì)算速度, 開啟fling
                mVelocityTracker.addMovement(vtev);
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                // 如果某個(gè)方向上的速度不為0就調(diào)用fling方法, 否則設(shè)置RecyclerView的狀態(tài)為IDLE
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
            } break;

            case MotionEvent.ACTION_CANCEL: {
                cancelScroll();
            } break;
        }
        return true;
    }

onTouchEvent()中的代碼進(jìn)行精簡畏铆,只留下處理嵌套滑動(dòng)的部分,整體的邏輯就清晰了起來。這里主要是對(duì)scroll的處理,關(guān)于fling的待會(huì)再看。
ACTION_DOWN的時(shí)候RecyclerView調(diào)用startNestedScroll()方法開始尋找可以進(jìn)行嵌套滑動(dòng)的父View锻离,其實(shí)內(nèi)部就是調(diào)用了NestedScrollingChildHelperstartNestedScroll()方法向上尋找最近的實(shí)現(xiàn)了NestedScrollingParent接口的父View并保存父View的引用疏虫。
ACTION_MOVE中執(zhí)行了嵌套滑動(dòng)關(guān)鍵的3步:一是由父View最先消耗滾動(dòng)距離dxdy翅敌;二是子View消耗剩余距離dx - mReusableIntPair[0]dy - mReusableIntPair[1];三是如果還有滾動(dòng)距離未消耗完,則再交給父Layout消耗饶深。

onTouchEvent()中進(jìn)行了第1步俱两,第2和第3步的邏輯在scrollByInternal()方法中:即首先讓RecyclerView自身滾動(dòng),再通過dispatchNestedScroll()將剩余的距離分發(fā)給父View,源碼精簡后如下。

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0; int unconsumedY = 0;
        int consumedX = 0; int consumedY = 0;

        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            // RecyclerView本身的滑動(dòng), 最終調(diào)用了LayoutManager的scrollHorizontallyBy()或scrollVerticallyBy()
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0]; consumedY = mReusableIntPair[1];
            unconsumedX = x - consumedX; unconsumedY = y - consumedY;
        }
        // ......
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        // ......
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

ACTION_UP的時(shí)候計(jì)算速度并調(diào)用fling()方法状蜗。一般來說我們通過Scroller來實(shí)現(xiàn)慣性滑動(dòng)缸血,在computeScroll()方法中不斷計(jì)算當(dāng)前的坐標(biāo)并移動(dòng)笆豁。不了解Scroller的可以看參考的[1~3]。
但是實(shí)現(xiàn)了NestedScrollingChild2接口的View有所不同媒抠,上面提到,這種View的Scroll和Fling事件都可以由dispatchNestedPreScroll()傳遞,由type參數(shù)區(qū)分事件類型苟耻,TYPE_TOUCH為Scroll事件解虱,TYPE_NON_TOUCH為Fling事件离咐。
......
是不是感覺怪怪的?按照方法的名字馆铁,dispatchNestedPreScroll()方法應(yīng)該只傳遞Scroll事件脱衙,而Fling事件由dispatchNestedPreFling()方法比較合理已旧。確實(shí)璃诀,對(duì)于只實(shí)現(xiàn)了NestedScrollingChild接口的View就是這么處理的,但是用這種方式傳遞速率比較粗暴犀变,在滑動(dòng)到邊界時(shí)可能存在卡頓現(xiàn)象。而實(shí)現(xiàn)了NestedScrollingChild2接口的View用了新的方式傳遞Fling事件,來看一下RecyclerView作為子View是怎么傳遞Fling事件給父View的。

    public boolean fling(int velocityX, int velocityY) {
        // ......
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            dispatchNestedFling(velocityX, velocityY, canScroll);

            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
                return true;
            }

            if (canScroll) {
                // ......
                startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

代碼中雖然調(diào)用了dispatchNestedPreFling()dispatchNestedFling()方法源梭,但是對(duì)于實(shí)現(xiàn)了NestedScrollingParent2的父View來說荠卷,對(duì)應(yīng)的回調(diào)方法都不實(shí)現(xiàn)也可以。
我們重點(diǎn)來看下面的mViewFlinger.fling(velocityX, velocityY),這句代碼實(shí)現(xiàn)了RecyclerView本身的慣性滑動(dòng)蚁堤,mViewFlinger是RecyclerView內(nèi)部類ViewFlinger的對(duì)象掂咒。該類精簡后的源碼如下:

    class ViewFlinger implements Runnable {
        @Override
        public void run() {
            // ......
            final OverScroller scroller = mOverScroller;
            if (scroller.computeScrollOffset()) {
                // Nested Pre Scroll
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
                        TYPE_NON_TOUCH)) {
                    unconsumedX -= mReusableIntPair[0];
                    unconsumedY -= mReusableIntPair[1];
                }
                // ......
                // Nested Post Scroll
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
                        TYPE_NON_TOUCH, mReusableIntPair);
                unconsumedX -= mReusableIntPair[0];
                unconsumedY -= mReusableIntPair[1];
                // ......
        }

        void postOnAnimation() {
            if (mEatRunOnAnimationRequest) {
                mReSchedulePostAnimationCallback = true;
            } else {
                internalPostOnAnimation();
            }
        }

        private void internalPostOnAnimation() {
            removeCallbacks(this);
            ViewCompat.postOnAnimation(RecyclerView.this, this);
        }

        public void fling(int velocityX, int velocityY) {
            setScrollState(SCROLL_STATE_SETTLING);
            mLastFlingX = mLastFlingY = 0;
            // Because you can't define a custom interpolator for flinging, we should make sure we
            // reset ourselves back to the teh default interpolator in case a different call
            // changed our interpolator.
            if (mInterpolator != sQuinticInterpolator) {
                mInterpolator = sQuinticInterpolator;
                mOverScroller = new OverScroller(getContext(), sQuinticInterpolator);
            }
            mOverScroller.fling(0, 0, velocityX, velocityY,
                    Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            postOnAnimation();
        }

        public void smoothScrollBy(int dx, int dy, int duration,
                @Nullable Interpolator interpolator) {
            // ......
            postOnAnimation();
        }

        public void stop() {
            removeCallbacks(this);
            mOverScroller.abortAnimation();
        }
    }

還記得我們平時(shí)通過Scroller是怎么實(shí)現(xiàn)慣性滑動(dòng)的嗎膝蜈?由于View每次draw()時(shí)會(huì)調(diào)用computeScroll()非剃,如果Scroller的滑動(dòng)尚未結(jié)束备绽,就在computeScroll()中計(jì)算當(dāng)前View應(yīng)該所處的scroll位置并移動(dòng)至該處,最后調(diào)用invalidate()繼續(xù)觸發(fā)draw()形成一個(gè)循環(huán)倍靡,直到慣性滑動(dòng)結(jié)束。

RecyclerView實(shí)現(xiàn)慣性滑動(dòng)和Fling事件傳遞的方式與之類似雨让,都是使用Scroller計(jì)算慣性滑動(dòng)的滑動(dòng)距離。但是并沒有重寫computeScroll()庵寞,那么循環(huán)調(diào)用的機(jī)制是在哪兒實(shí)現(xiàn)的呢?

這里就不得不提postOnAnimation()的作用了古沥,其內(nèi)部調(diào)用了ViewCompat.postOnAnimation(View, Runnable)岩齿,它會(huì)將當(dāng)前這個(gè)Runnable對(duì)象post到Choreographer的執(zhí)行隊(duì)列中,等到下一幀到來的時(shí)候會(huì)執(zhí)行該Runnnable對(duì)象的run()方法乞封。也就是說菇用,每一幀刷新的時(shí)候都會(huì)通過Scroller計(jì)算這一幀應(yīng)該滑動(dòng)的距離dxdy,然后開啟嵌套滑動(dòng),只不過此時(shí)的type不是TYPE_TOUCH飞蚓,而是TYPE_NON_TOUCH,對(duì)于60幀的手機(jī)著榴,一秒會(huì)分發(fā)60次的嵌套滑動(dòng)事件。

1.4 吸頂效果代碼

上面說了這么多问麸,可以發(fā)現(xiàn)RecyclerView本身為嵌套滑動(dòng)做了很多事情,如果以RecyclerView作為嵌套滑動(dòng)的子View哮笆,父View實(shí)現(xiàn)onNestedPreScroll()就可以實(shí)現(xiàn)初步的嵌套滑動(dòng)效果。想要實(shí)現(xiàn)吸頂效果的代碼启具,我們自定義繼承自LinearLayout的SimpleNestedLinearLayout作為父View并重寫onNestedPreScroll()方法如下拷沸。

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        if (dy > 0 && scrollY < imageViewHeight) {
            var actualDy = dy
            if (scrollY + dy >= imageViewHeight) {
                actualDy = imageViewHeight - scrollY
            }
            consumed[1] = actualDy
            scrollBy(0, actualDy)
        } else if (dy < 0 && recyclerView?.canScrollVertically(-1) == false && scrollY > 0) {
            var actualDy = dy
            if (scrollY + dy < 0) {
                actualDy = -scrollY
            }
            consumed[1] = actualDy
            scrollBy(0, actualDy)
        }
    }

這里還需要重寫onMeasure()方法,因?yàn)長inearLayout本身的measure流程不符合吸頂效果的需求:LinearLayout會(huì)依次measure子View,然后將剩余的高度作為之后子View的最大高度帝嗡,如果這里不重寫onMeasure()方法,RecyclerView的高度就=(LinearLayout高度-ImageView高度-TextView高度)巢寡,但是RecyclerView的高度應(yīng)該=(父View的高度-TextView高度)。

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        setMeasuredDimension(
            getDefaultSize(suggestedMinimumWidth, widthMeasureSpec),
            getDefaultSize(suggestedMinimumHeight, heightMeasureSpec)
        )
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        for (index in 0 until childCount) {
            val child = getChildAt(index) ?: continue
            val layoutParams = child.layoutParams
            if (layoutParams.height == LayoutParams.MATCH_PARENT) {
                val rvHeight = measuredHeight - resources
                                    .getDimensionPixelSize(R.dimen.simple_nested_title_height)
                val childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(rvHeight, heightMode)
                measureChild(child, widthMeasureSpec, childHeightMeasureSpec)
            } else {
                val childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, heightMode)
                measureChild(child, widthMeasureSpec, childHeightMeasureSpec)
            }
        }
    }

剩下的我就不多說了,大家下載源碼查看吧挨稿。

二、WebView與RecyclerView混合布局

2.1 效果展示

WebView與RecyclerView的混合布局經(jīng)常用于新聞APP的新聞頁,其中WebView展示的新聞本身的網(wǎng)頁钉赁,下方RecyclerView負(fù)責(zé)展示相關(guān)推薦、廣告带膜、評(píng)論等內(nèi)容式廷。

WebView&RecyclerView.gif

2.2 實(shí)現(xiàn)原理

可以發(fā)現(xiàn)這個(gè)父View也是一個(gè)類似LinearLayout的垂直布局,WebView和RecyclerView的高度都與父View相等穗慕。
當(dāng)用戶劃動(dòng)WebView時(shí)逛绵,WebView本身并沒有移動(dòng)瓢对,而是調(diào)用WebView.scrollBy(...)移動(dòng)WebView里面的內(nèi)容。直到WebView的內(nèi)容滑動(dòng)到底部時(shí)法焰,調(diào)用父View的scrollBy(...)將WebView和RecyclerView向上移動(dòng)。此時(shí)的布局如下所示,黑色框表示父View傻丝,是用戶的可見區(qū)域。

混合布局.png

WebView本身并不支持嵌套滑動(dòng)诉儒,因此我們需要自定義繼承自WebView的NestedScrollWebView葡缰,并重寫它的onTouchEvent()方法,將它的滑動(dòng)事件向外分發(fā),這部分邏輯參照RecyclerView即可运准,這里不再贅述幌氮,可以參考本文開頭的鏈接胁澳。值得注意的是该互,WebView需要判斷自己的內(nèi)容是否已經(jīng)滑動(dòng)到底部,因此在NestedScrollWebView添加如下方法韭畸。

    fun canWebViewScrollDown(): Boolean {
        val range = computeVerticalScrollRange()
        return ((scrollY + measuredHeight) < range)
    }

在WebView與RecyclerView混合布局中宇智,主要關(guān)注父View的邏輯。我們定義一個(gè)繼承自ViewGroup的MixedLayout胰丁,首先重寫它的onMeasure()方法随橘,把WebView和RecyclerView的高度都設(shè)置為父View的高度,再重寫它的onLayout()方法锦庸,按照垂直線性布局的方式去排布机蔗。

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        setMeasuredDimension(
            getDefaultSize(suggestedMinimumWidth, widthMeasureSpec),
            getDefaultSize(suggestedMinimumHeight, heightMeasureSpec)
        )
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val height = measuredHeight
        for (i in 0 until childCount) {
            val child = getChildAt(i) ?: continue
            // 其實(shí)WebView的高度不一定為父View的高度, 因?yàn)橛行┒绦侣劦母叨炔蛔阋黄?            // 這里為了方便, 假定新聞都是超過一屏的
            if (child == webView) {
                val childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, heightMode)
                measureChild(webView, widthMeasureSpec, childHeightMeasureSpec)
            } else if (child == recyclerView) {
                val childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, heightMode)
                measureChild(recyclerView, widthMeasureSpec, childHeightMeasureSpec)
            } else {
                measureChild(child, widthMeasureSpec, heightMeasureSpec)
            }
        }
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child == webView) {
                child.layout(0, 0, measuredWidth, measuredHeight)
            } else if (child == recyclerView) {
                child.layout(0, measuredHeight, measuredWidth, measuredHeight * 2)
            }
        }
        layoutMaxScrollY = measuredHeight
    }

下面來看MixedLayout處理嵌套滑動(dòng)的邏輯,首先來看onNestedPreScroll()方法甘萧。該方法使用actualDy表示實(shí)際移動(dòng)的距離萝嘁,用于處理邊界滑動(dòng)。主要邏輯為判斷當(dāng)前是哪個(gè)View在分發(fā)嵌套滑動(dòng)事件扬卷,再根據(jù)滑動(dòng)方向分別進(jìn)行處理牙言。

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        var actualDy = dy
        if (target == webView) {
            if (dy > 0 && !webView.canWebViewScrollDown()) {
                // WebView內(nèi)容向下滑動(dòng)
                if (scrollY + actualDy > layoutMaxScrollY) {
                    actualDy = layoutMaxScrollY - scrollY
                }
                scrollBy(0, actualDy)
                consumed[1] = actualDy
            } else if (dy < 0 && scrollY > 0) {
                // WebView內(nèi)容向上滑動(dòng)
                if (scrollY + actualDy < 0) {
                    actualDy = -scrollY
                }
                scrollBy(0, actualDy)
                consumed[1] = actualDy
            }
        } else if (target == recyclerView) {
            if (dy > 0 && scrollY < layoutMaxScrollY) {
                if (scrollY + actualDy > layoutMaxScrollY) {
                    actualDy = layoutMaxScrollY - scrollY
                }
                scrollBy(0, actualDy)
                consumed[1] = actualDy
            } else if (dy < 0) {
                if (!recyclerView.canScrollVertically(-1)) {
                    if (scrollY + actualDy < 0) {
                        actualDy = -scrollY
                        webView.stopFling()
                    }
                    scrollBy(0, actualDy)
                    consumed[1] = actualDy
                }
            }
        }
    }

還有一種情況是在WebView觸發(fā)一個(gè)速度較大的fling,這時(shí)WebView的內(nèi)容會(huì)滑動(dòng)到底部怪得,隨后MixedLayout也會(huì)滑動(dòng)到底部咱枉,最后開始滑動(dòng)RecyclerView。這種情況下的滑動(dòng)事件會(huì)分發(fā)到onNestedScroll()中進(jìn)行處理徒恋,具體如下所示蚕断。

    /**
     * RecyclerView的Fling事件傳遞到了WebView 或 WebView的Fling事件傳遞到了RecyclerView
     */
    override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int,
                                dxUnconsumed: Int, dyUnconsumed: Int, type: Int, consumed: IntArray) {
        if (dyUnconsumed == 0 || nestedScrollAxes != ViewCompat.SCROLL_AXIS_VERTICAL) {
            return
        }
        consumed[1] = dyUnconsumed
        if (target == webView && dyUnconsumed > 0) {
            if (scrollY >= layoutMaxScrollY) {
                if (scrollY > layoutMaxScrollY) {
                    scrollTo(0, layoutMaxScrollY)
                }
                if (recyclerView.canScrollVertically(1)) {
                    recyclerView.scrollBy(0, dyUnconsumed)
                } else {
                    webView.stopNestedScroll(type)
                }
            }
        } else if (target == recyclerView && dyUnconsumed < 0) {
            if (scrollY <= 0) {
                if (scrollY < 0) {
                    scrollTo(0, 0)
                }
                if (webView.scrollY + dyUnconsumed > 0) {
                    webView.scrollBy(0, dyUnconsumed)
                } else {
                    recyclerView.stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)
                    webView.scrollTo(0, 0)
                }
            }
        }
    }

三、回彈布局

3.1 效果展示

列表回彈的效果如下所示因谎,在列表滑動(dòng)到邊緣時(shí)可以超過RecyclerView的滑動(dòng)邊界基括,并在用戶松手后回彈至原本的邊界,這種行為也被稱為OverScroll财岔。

gif-列表回彈.gif

3.2 實(shí)現(xiàn)原理

從現(xiàn)象上來看是用戶在滑動(dòng)到RecyclerView的邊界之后還可以多滑動(dòng)一段距離风皿,并在用戶松手時(shí)觸發(fā)回彈,但實(shí)際上實(shí)現(xiàn)OverScroll的不是RecyclerView本身匠璧,而是它的父View桐款,我們不需要對(duì)RecyclerView做任何改變,只需要在它外面套一個(gè)支持OverScroll的BounceLayout即可夷恍。布局如下所示魔眨,黑色框?yàn)锽ounceLayout,藍(lán)色框?yàn)镽ecyclerView。

圖片-回彈布局.png

RecyclerView本身實(shí)現(xiàn)了嵌套滑動(dòng)遏暴,當(dāng)它滑動(dòng)到邊界時(shí)侄刽,經(jīng)常會(huì)產(chǎn)生未消耗的滑動(dòng)距離,也就是dyUnconsumed朋凉,并通過dispatchNestedScroll(...)將這段距離分發(fā)給BounceLayout進(jìn)行處理州丹,BounceLayout即可通過scrollBy(...)滑動(dòng)自己來達(dá)到OverScroll的效果。用戶松手時(shí)RecyclerView會(huì)調(diào)用stopNestedScroll()杂彭,此時(shí)BounceLayout進(jìn)行回彈即可墓毒。

上面說的是用戶拖動(dòng)RecyclerView時(shí)的情況,在慣性滑動(dòng)下亲怠,如果fling到了邊界所计,那么BounceLayout需要在RecyclerView fling到邊界時(shí)計(jì)算當(dāng)前的速率,根據(jù)速率向外彈出一段距離团秽,最終在速度為0時(shí)回彈主胧。

了解原理之后可以發(fā)現(xiàn)BounceLayout不僅僅可以用于實(shí)現(xiàn)RecyclerView的回彈,任何像RV一樣實(shí)現(xiàn)了嵌套滑動(dòng)子View功能的視圖都可以實(shí)現(xiàn)該功能徙垫,因此這種實(shí)現(xiàn)方式具有很好的解耦性讥裤。下面來看具體實(shí)現(xiàn)。

3.3 具體實(shí)現(xiàn)

3.3.1 最大OverScroll距離

先來討論一下如何限制OverScroll的滑動(dòng)距離姻报,定義當(dāng)前OverScroll的距離為OverScrollDistance,最大可滑動(dòng)距離為MaxOverScrollDistance间螟。假設(shè)當(dāng)前用戶下拉y吴旋,則BounceLayout調(diào)用scrollBy(-y)使其整體向下移動(dòng),當(dāng)BounceLayout的Math.abs(scrollY) == MaxOverScrollDistance時(shí)厢破,不管用戶怎么下拉荣瑟,BounceLayout也不該再移動(dòng)了。

上面描述的是OverScrollDistance=scrollY摩泪,也就是線性關(guān)系時(shí)的效果:此時(shí)用戶下拉dy笆焰,BounceLayout移動(dòng)dy。不過如果你使用過OverScroll的功能你就知道见坑,你下拉的距離和BounceLayout移動(dòng)的距離并不是線性關(guān)系:當(dāng)你下拉y時(shí)嚷掠,當(dāng)前OverScrollDistance越大,BounceLayout的實(shí)際移動(dòng)距離就越小荞驴,說得通俗一點(diǎn):當(dāng)前已經(jīng)滑動(dòng)的距離越大不皆,你越難滑動(dòng)它。

想要實(shí)現(xiàn)這樣的效果并不難熊楼,我們?yōu)镺verScrollDistance和scrollY定義一個(gè)插值器OverScrollerInterpolator
input = OverScrollDistance/MaxOverScrollDistance
output = scrollY/MaxOverScrollDistance霹娄,公式為:output = (1 - factor ^ (input * 2)),當(dāng)factor為0.6時(shí),函數(shù)圖如下所示犬耻。

圖片-overScrollDistance和scrollY.png

該函數(shù)是先快后慢的效果踩晶,越臨近最大值,用戶越難拖動(dòng)枕磁,這能給用戶帶來較好的體驗(yàn)合瓢。而且不管input多大,output始終<1透典,因此Math.abs(scrollY)永遠(yuǎn)<MaxOverScrollDistance晴楔。根據(jù)該公式,我們可以定義如下插值器:

    inner class OverScrollerInterpolator(private var factor: Float) : Interpolator {

        fun getInterpolationBack(input: Float) : Float {
            return (ln(1 - input) / ln(factor) / 2)
        }

        override fun getInterpolation(input: Float): Float {
            return (1 - factor.pow(input * 2))
        }
    }

令x = OverScrollDistance/MaxOverScrollDistance
令y = scrollY/MaxOverScrollDistance
當(dāng)已知x時(shí)峭咒,可以通過getInterpolation()計(jì)算y税弃,那么已知y時(shí),該怎么計(jì)算x呢凑队?我們來算一下:

  y = 1 - factor ^ x* 2
=>x * 2 = log(factor, 1 - y)
=>x * 2 = log(2, 1 - y) / log(2, factor)
=>x = (log(2, 1 - y) / log(2, factor)) / 2

得到的結(jié)果就是getInterpolationBack()里的算式则果。

至此,我們可以通過OverScrollerInterpolator中的兩個(gè)方法建立scrollY和overScrollDistance之間的函數(shù)關(guān)系漩氨,demo中取factor為0.6西壮,新建插值器如下:

private val mInterpolator: OverScrollerInterpolator = OverScrollerInterpolator(0.6f)

對(duì)于這個(gè)插值器的用法,我們舉個(gè)例子:用戶滑動(dòng)RecyclerView到邊界時(shí)叫惊,BounceLayout可以在onNestedScroll(...)方法中處理dyUnconsumed款青,如下所示,我們將未消耗的滑動(dòng)距離dyUnconsumed加到mOverScrollDistance并通過mInterpolator的getInterpolation()方法將其轉(zhuǎn)化成scrollY霍狰,再調(diào)用scrollTo()移動(dòng)到最終的位置抡草。

    override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, 
                dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
        if (mOrientation == ViewCompat.SCROLL_AXIS_VERTICAL && dyUnconsumed != 0) {
            if (type == ViewCompat.TYPE_TOUCH) {
                startOverScroll(dyUnconsumed)
            } else {
                ......
            }
        }
    }

    private fun startOverScroll(dy: Int) {
        updateOverScrollDistance(mOverScrollDistance + dy)
    }

    private fun updateOverScrollDistance(distance: Int) {
        mOverScrollDistance = distance
        if (mOverScrollDistance < 0) {
            scrollTo(
                0, (-mMaxOverScrollDistance * mInterpolator.getInterpolation(
                    abs(mOverScrollDistance.toFloat() / mOverScrollBorder)
                )).toInt()
            )
        } else {
            scrollTo(
                0, (mMaxOverScrollDistance * mInterpolator.getInterpolation(
                    abs(mOverScrollDistance.toFloat() / mOverScrollBorder)
                )).toInt()
            )
        }
    }
3.3.2 SpringBack

SpringBack指回彈,表現(xiàn)為將當(dāng)前處于OverScroll狀態(tài)下的BounceLayout恢復(fù)到初始狀態(tài)蔗坯,我們選擇使用ValueAnimator實(shí)現(xiàn)該功能康震,當(dāng)需要回彈時(shí)听盖,調(diào)用startScrollBackAnimator ()方法即可售睹,相關(guān)代碼如下。

    private var mSpringBackAnimator: ValueAnimator? = null
    private val mMaxOverScrollDistance = 300
    // mOverScrollBorder為mMaxOverScrollDistance的n倍
    // 主要用于優(yōu)化滑動(dòng)體驗(yàn)称勋,n越大绘梦,滑動(dòng)阻力越大
   private val mOverScrollBorder = mMaxOverScrollDistance * 3

    fun startScrollBackAnimator() {
        mSpringBackAnimator?.cancel()
        mSpringBackAnimator = ValueAnimator.ofInt(mOverScrollDistance, 0)
        mSpringBackAnimator?.interpolator = DecelerateInterpolator()
        mSpringBackAnimator?.duration = SPRING_BACK_DURATION.toLong()
        mSpringBackAnimator?.addUpdateListener { animation ->
            updateOverScrollDistance(
                animation.animatedValue as Int
            )
        }
        mSpringBackAnimator?.start()
    }

    private fun updateOverScrollDistance(distance: Int) {
        ......
    }

當(dāng)回彈時(shí)橘忱,建立一個(gè)value從mOverScrollDistance到0的ValueAnimator,更新value時(shí)調(diào)用updateOverScrollDistance()谚咬,通過mInterpolator將mOverScrollDistance轉(zhuǎn)化成scrollY并移動(dòng)至該位置鹦付。

可以看到實(shí)現(xiàn)回彈效果的邏輯比較簡單,有難度的點(diǎn)在于我們應(yīng)該在什么時(shí)候觸發(fā)回彈择卦。有如下3種場(chǎng)景:
第1種場(chǎng)景: 用戶拖動(dòng)RecyclerView至OverScrollDistance>0后松手敲长。
此時(shí)RecyclerViewACTION_UP郎嫁,調(diào)用stopNestedScroll(),回調(diào)至BounceLayout中的onStopNestedScroll()方法祈噪,在該方法中即可進(jìn)行回彈泽铛。

    override fun onStopNestedScroll(target: View, type: Int) {
        mNestedScrollingParentHelper.onStopNestedScroll(target)
        if (mOverScrollDistance != 0) {
            startScrollBackAnimator()
        }
    }

第2種場(chǎng)景: 用戶拖動(dòng)RecyclerView至OverScrollDistance>0后,再觸發(fā)fling后松手辑鲤。
此時(shí)BounceLayout應(yīng)該再順著fling滑動(dòng)很小一段距離后開始回彈盔腔,我們來看一下代碼實(shí)現(xiàn)。

    override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int,
        dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
        if (mOrientation == ViewCompat.SCROLL_AXIS_VERTICAL && dyUnconsumed != 0) {
            if (type == ViewCompat.TYPE_TOUCH) { // 用戶在拖動(dòng)RV
                startOverScroll(dyUnconsumed)
            } else { // RV在fling狀態(tài)
                if (mOverScrollDistance == 0) { // Bounce月褥,下節(jié)說明
                    mScroller.computeScrollOffset()
                    startBounceAnimator(mScroller.currVelocity * mLastSign)
                } else { // 當(dāng)前場(chǎng)景
                    startOverScroll(dyUnconsumed) // 順著當(dāng)前fling的方向再滑動(dòng)一小段距離
                }
                // 讓RecyclerView主動(dòng)停止嵌套滑動(dòng)
                ViewCompat.stopNestedScroll(target, type)
            }
        }
    }

可以看到在當(dāng)前場(chǎng)景下弛随,BounceLayout會(huì)再移動(dòng)一小段距離,隨后主動(dòng)調(diào)用ViewCompat.stopNestedScroll(target, type)宁赤,此時(shí)會(huì)回調(diào)至BounceLayout的onStopNestedScroll(...)開始回彈舀透。

第3種場(chǎng)景: RecyclerView慣性滑動(dòng)至邊界,BounceLayout根據(jù)當(dāng)前速率外彈出一段距離决左,直到速率為0時(shí)回彈愕够,這種行為就被稱為Bounce。上一段代碼中佛猛,當(dāng)滑動(dòng)到邊界且mOverScrollDistance == 0時(shí)觸發(fā)Bounce惑芭,具體的邏輯來看下一節(jié)。

3.3.3 Bounce

在上一節(jié)的代碼中我們看到觸發(fā)Bounce的代碼如下:

mScroller.computeScrollOffset()
startBounceAnimator(mScroller.currVelocity * mLastSign)

通過startBounceAnimator()觸發(fā)Bounce需要初速度和方向继找,我們可以在onNestedPreFling()中得到RecyclerView慣性滑動(dòng)時(shí)的初速度velocityY和方向mLastSign遂跟。

    override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
        mLastSign = if (velocityY < 0) -1 else 1
        mScroller.forceFinished(true)
        mScroller.fling(0, 0, 0, velocityY.toInt(), 0,
            Int.MAX_VALUE, 0, Int.MAX_VALUE)
        return false
    }

但是RecyclerView慣性滑動(dòng)的初速度很顯然不等于觸發(fā)Bounce時(shí)的初速度,因此我們通過mScroller.fling()計(jì)算速度码荔,在滑動(dòng)至邊界時(shí)調(diào)用mScroller.computeScrollOffset()計(jì)算當(dāng)前時(shí)間點(diǎn)的速度漩勤,再通過mScroller.getCurrVelocity()即可得到觸發(fā)Bounce時(shí)的初速度。

得到初速度和方向后我們來看看startBounceAnimator(...)做了什么缩搅。

    private fun startBounceAnimator(velocity: Float) {
        mBounceRunnable?.cancel()
        mBounceRunnable = BounceAnimRunnable(velocity, mOverScrollDistance)
        mBounceRunnable?.start()
    }

該方法啟動(dòng)了BounceAnimRunnable,來看一下它的代碼触幼。在構(gòu)造函數(shù)中硼瓣,首先根據(jù)初速度mVelocity÷減速度mDeceleration計(jì)算duration。啟動(dòng)BounceAnimRunnable后每隔FRAME_TIME毫秒計(jì)算一次當(dāng)前的mOverScrollDistance置谦,當(dāng)duration結(jié)束時(shí)通過startScrollBackAnimator ()回彈堂鲤。

    inner class BounceAnimRunnable : Runnable {
        /**
         * 兩幀之間的間隔
         */
        private var frameInternalMillis = 10

        private var mDeceleration = 0
        private var mVelocity = 0f
        private var mStartY = 0
        private var mRuntime = 0
        private var mDuration = 0
        private var mHasCanceled = false

        constructor(velocity: Float, startY: Int) {
            mDeceleration = if (velocity < 0) {
                BOUNCE_BACK_DECELERATION
            } else {-BOUNCE_BACK_DECELERATION
            }
            mVelocity = velocity
            mStartY = startY
            mDuration = ((-mVelocity / mDeceleration) * 1000).toInt()
        }

        fun start() {
            postDelayed(this, frameInternalMillis.toLong())
        }

        fun cancel() {
            mHasCanceled = true
            removeCallbacks(this)
        }

        override fun run() {
            if (mHasCanceled) {
                return
            }
            mRuntime += frameInternalMillis
            val t = mRuntime.toFloat() / 1000
            val distance = (mStartY + mVelocity * t + 0.5 * mDeceleration * t * t).toInt()
            updateOverScrollDistance(distance)
            if (mRuntime < mDuration && abs(distance) < mMaxOverScrollDistance * 2) {
                removeCallbacks(this)
                postDelayed(this, frameInternalMillis.toLong())
            } else {
                startScrollBackAnimator()
            }
        }

    }

至此,列表回彈的基本邏輯就講完了媒峡,限于篇幅瘟栖,有些細(xì)節(jié)并未全部列出。完整的源代碼就不貼了谅阿,感興趣的同學(xué)可以去文章開頭的地址下載半哟。

四酬滤、參考

  1. Android Scroller解析和使用
  2. Scroller的使用及解析
  3. Android Scroller完全解析,關(guān)于Scroller你所需知道的一切
  4. Android嵌套滑動(dòng)機(jī)制實(shí)戰(zhàn)演練
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寓涨,一起剝皮案震驚了整個(gè)濱河市盯串,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌戒良,老刑警劉巖体捏,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異糯崎,居然都是意外死亡几缭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門沃呢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來年栓,“玉大人,你說我怎么就攤上這事樟插≡涎螅” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵黄锤,是天一觀的道長搪缨。 經(jīng)常有香客問我,道長鸵熟,這世上最難降的妖魔是什么副编? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮流强,結(jié)果婚禮上痹届,老公的妹妹穿的比我還像新娘。我一直安慰自己打月,他們只是感情好队腐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奏篙,像睡著了一般柴淘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上秘通,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天为严,我揣著相機(jī)與錄音,去河邊找鬼肺稀。 笑死第股,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的话原。 我是一名探鬼主播夕吻,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼诲锹,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了梭冠?” 一聲冷哼從身側(cè)響起辕狰,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎控漠,沒想到半個(gè)月后蔓倍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盐捷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年偶翅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碉渡。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡聚谁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滞诺,到底是詐尸還是另有隱情形导,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布习霹,位于F島的核電站朵耕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏淋叶。R本人自食惡果不足惜阎曹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望煞檩。 院中可真熱鬧处嫌,春花似錦、人聲如沸斟湃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凝赛。三九已至癣缅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哄酝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國打工祷膳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陶衅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓直晨,卻偏偏與公主長得像搀军,于是被迫代替她去往敵國和親膨俐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容