事件分發不夠,NestedScrolling來湊

前言

前倆篇文章,我們聊了事件分發的原理。經過原理和工做經驗,咱們明白僅靠熟知事件分發遠遠不足以作出細膩的用戶體驗。java

就好比最多見的一個場景:數組

很明顯,若是想要實現這個效果,經過常規的事件分發機制很顯然是沒辦法實現的。畢竟上面的Bar一旦開始滑動,說明它已經肯定消費此事件,那麼在一次滑動中,下面的RecyclerView不管如何也拿不到這次事件。ide

**可是!**既然RecyclerView + CoordinatorLayout實現了這個效果,那就說明有方法作。這個方法也就是今天要聊的NestedScrolling機制~~~源碼分析

內容簡介:這篇文章不聊用法,主要進行源碼分析~~~學習

#正文ui

若是咱們瞭解事件分發機制,那麼咱們就會很清楚,事件分發存在的弊端:一旦一個View消費此事件,那麼這個消費事件序列將徹底有此View承包了。所以咱們根本不可能作到一個View消費一半的MOVE事件,而後把餘下的MOVE事件給別人。this

爲了解決這個問題,Google仍然基於事件分發的思想,在事件分發的流程中增長了NestedScrolling機制。提起來很洋氣,說白了就是倆個接口:NestedScrollingParentNestedScrollingChildspa

固然較新的SDK會發現這個接口變成了NestedScrollingParent2NestedScrollingChild2NestedScrollingParent2繼承自NestedScrollingParent。所以咱們文章也是基於NestedScrollingParent/NestedScrollingChild進行分析的。代理

此機制其實異常的簡單,總結起來就是一句話:實現了NestedScrollingChild接口的內部View在滑動的時候,首先將滑動距離dx和dy交給實現了NestedScrollingParent接口的外部View(能夠不是直接父View),外部View可對其進行部分消耗,剩餘的部分再還給內部Viewcode

NestedScrollingParent

第一次點進這個接口...wtf?這麼多方法?...不過冷靜下來,其實很簡單。

public interface NestedScrollingParent {
    // 開始滑動時被調用。返回值表示是否消費內部View滑動時的參數(x、y)。
    boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    //接收內部View(能夠是非直接子View)滑動
    void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

   //中止消費內部View的事件
    void onStopNestedScroll(View target);
    
    // 內部View滑動時調用
    void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);

    //內部View開始滑動以前調用。參數dx和dy表示滑動的橫向和縱向距離,
    //consumed參數表示消耗的橫向和縱向距離,如縱向滑動,須要消耗了dy/2,
    //表示外部View和內部View分別處理此次滑動距離的 1/2
    void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    //內部View開始Fling時調用
    boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    //內部View開始Fling以前調用。參數velocityX 和velocityY 表示水平方向和垂直方向的速度。
    //返回值表示是否處理了此次Fling操做,返回true表示攔截掉此次操做,false表示不攔截。
    boolean onNestedPreFling(View target, float velocityX, float velocityY);

    //縱向滑動或橫向滑動
    int getNestedScrollAxes();
}
複製代碼

NestedScrollingChild

public interface NestedScrollingChild {

    // 設置是否支持NestedScrolling
    void setNestedScrollingEnabled(boolean enabled);
    
    boolean isNestedScrollingEnabled();

    //準備開始滑動
    boolean startNestedScroll(int axes);
    
    //中止滑動
    void stopNestedScroll();

   //是否有嵌套滑動的外部View
    boolean hasNestedScrollingParent();
   
    //在內部View滑動的時候,通知外部View。
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
    
    //在內部View滑動以前,先讓外部View處理此次滑動。
    //參數dx 和 dy表示此次滑動的橫向和縱向距離,參數consumed表示外部View消耗此次滑動的橫向和縱向距離。
    //返回值表示外部View是否有消耗此次滑動。
    boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
   
   //在內部View進行Fling操做時,通知外部View。
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
    
   //與dispatchNestedPreScroll 方法類似...
    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
複製代碼

解讀

方法不少,可是真的真的真的很簡單!!!

我猜認真看過每個方法命名的小夥伴,甚至已經猜到這套機制是怎麼實現的了。

接下來的解讀,直接根據實現代碼,來完全捋清楚NestedScrollingParentNestedScrollingChild這麼多方法的用意。

首先,NestedScrolling機制,是有內向外,由子向父進行「試探性詢問」的這麼一個機制。所以我們先從實現了NestedScrollingChildRecyclerView看起。

1、RecyclerView

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 複製代碼

1.一、startNestedScroll()過程

onInterceptTouchEvent()咱們能夠看到startNestedScroll()在DOWN事件出現的時候被調用:

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
    // 省略部分代碼
    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            // 調用startNestedScroll()
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            break;
        // 省略部分代碼
    // retrun值取決於當前RecyclerView是否滑動
    return mScrollState == SCROLL_STATE_DRAGGING;
}
複製代碼

點進startNestedScroll()後,咱們會發現具體實現被代理到NestedScrollingChildHelper中:

@Override
public boolean startNestedScroll(int axes, int type) {
    return getScrollingChildHelper().startNestedScroll(axes, type);
}
複製代碼

而Helper內部,經過getParent()拿到父View,而後調用NestedScrollingParent2中的onStartNestedScroll()

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    // 省略部分代碼
    // 是否啓動NestedScrolling
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            // 若是Parent不爲null,調用父類的onStartNestedScroll()方法,若是此方法返回true,則直接return true
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
                // 若是if爲true,則調用onNestedScrollAccepted()
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}
複製代碼
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);
        // 省略部分代碼
複製代碼

CoordinatorLayout(實現了NestedScrollingParent2)中的onStartNestedScroll()會發現,CoordinatorLayout又將此方法轉到了Behavior之中。此時方法的返回值取決於Behavior之中onStartNestedScroll()的返回值。

我用的是AppBarLayout,因此此時的Behavior的實如今AppBarLayout中。

@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
    boolean handled = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        // 省略部分代碼
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                    target, axes, type);
            handled |= accepted;
            lp.setNestedScrollAccepted(type, accepted);
        } else {
            lp.setNestedScrollAccepted(type, false);
        }
    }
    return handled;
}
複製代碼

若是返回true,那麼就意味着Behavior中的setNestedScrollAccepted()被調用。此方法在CoordinatorLayout有一個默認實現,說白了就是一個成員變量賦值爲true。

void setNestedScrollAccepted(int type, boolean accept) {
    switch (type) {
        case ViewCompat.TYPE_TOUCH:
            mDidAcceptNestedScrollTouch = accept;
            break;
        case ViewCompat.TYPE_NON_TOUCH:
            mDidAcceptNestedScrollNonTouch = accept;
            break;
    }
}
複製代碼

這個變量說白了,就是記錄某個子View可以響應NestedScrolling。

1.二、dispatchNestedPreScroll()過程

接下來咱們看點「真刀真槍」的東西。

@Override
public boolean onTouchEvent(MotionEvent e) {
    // 省略部分代碼
    switch (action) {
        // 省略部分代碼
        case MotionEvent.ACTION_MOVE: {
            // 省略部分代碼
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // Updated the nested offsets
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
            }
        }
        // 省略部分代碼
    }
    return true;
}
複製代碼

咱們能夠看到,在onTouchEvent()中的MOVE事件中,RecyclerView調用了dispatchNestedPreScroll()

而此時也意味着RecyclerView開始消費此事件。

咱們能夠看出dispatchNestedPreScroll()方法一樣經過NestedScrollingChildHelper,而後到ViewParentCompat轉到了CoordinatorLayoutonNestedPreScroll()中。

CoordinatorLayout一樣經過找到能夠響應的Behavior,調用其onNestedPreScroll()的實現。

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
    // 遍歷子View
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        // 省略部分代碼
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        // 判斷當前View是否可以響應NestedScrolling,也就是我們startNestedScroll()過程當中設置的值
        if (!lp.isNestedScrollAccepted(type)) {
            continue;
        }

        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            mTempIntPair[0] = mTempIntPair[1] = 0;
            viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
            // 省略部分代碼
        }
    }
    // 省略部分代碼
}
複製代碼

AppBarLayout中的實現是這樣的:

public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, T child, View target, int dx, int dy, int[] consumed, int type) {
    if(dy != 0) {
        int min;
        int max;
        if(dy < 0) {
            min = -child.getTotalScrollRange();
            max = min + child.getDownNestedPreScrollRange();
        } else {
            min = -child.getUpNestedPreScrollRange();
            max = 0;
        }

        if(min != max) {
            // 調用scroll,滑動本身並把消費了的dy,經過數組傳回去。
            consumed[1] = this.scroll(coordinatorLayout, child, dy, min, max);
            // 判斷是否須要stop
            this.stopNestedScrollIfNeeded(dy, child, target, type);
        }
    }
}
複製代碼

執行到CoordinatorLayout中的時候,不知道有小夥伴注意到嗎,這一系列的方法的返回值已經爲void了。由於dispatchNestedPreScroll()的返回值在Helper中進行處理:

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
    if (isNestedScrollingEnabled()) {
        final ViewParent parent = getNestedScrollingParentForType(type);

        if (dx != 0 || dy != 0) {
            // 省略部分代碼
            ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
            // 省略部分代碼,只要consumed的0、1位不爲0,即返回true
            return consumed[0] != 0 || consumed[1] != 0;
            // 省略部分代碼
        }
        return false;
    }
}
複製代碼

對於RecyclerView來講,dispatchNestedPreScroll()返回ture,則意味着這次MOVE事件被上級某個View消費了,接下來對於本身來講的就是根據剩餘的事件作一些本身該作的消費。

if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
    // dx、dy減去其餘View消費的dx、dy剩下的也就是本身可以消費的事件了。
    dx -= mScrollConsumed[0];
    dy -= mScrollConsumed[1];
    // 省略部分代碼
}
複製代碼

1.三、dispatchNestedScroll()過程

此方法在RecyclerView自身有滑動動做的時候被調用:

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    // 省略部分代碼,dispatchNestedScroll()被調用
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
            TYPE_TOUCH)) {
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        if (ev != null) {
            ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        }
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
    }
    // 省略自身滑動的業務代碼
    return consumedX != 0 || consumedY != 0;
}
複製代碼

.........

毫無疑問,此方法又會最終調用到AppBarLayout中的Behavior中:

public void onNestedScroll(CoordinatorLayout coordinatorLayout, T child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
    if(dyUnconsumed < 0) {
        this.scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
        this.stopNestedScrollIfNeeded(dyUnconsumed, child, target, type);
    }

    if(child.isLiftOnScroll()) {
        child.setLiftedState(target.getScrollY() > 0);
    }

}
複製代碼

OK,就這樣本來屬於RecyclerView的事件,硬生生的傳遞給了被人。只能「玩」別人「玩」剩下的事件...

1.四、stopNestedScroll()過程

既然是stop,那必然是Parent主動發起了,沒錯上述過程當中stopNestedScrollIfNeeded(dyUnconsumed, child, target, type)被調用,即意味着嘗試stop:

private void stopNestedScrollIfNeeded(int dy, T child, View target, int type) {
    if(type == 1) {
        int curOffset = this.getTopBottomOffsetForScrollingSibling();
        if(dy < 0 && curOffset == 0 || dy > 0 && curOffset == -child.getDownNestedScrollRange()) {
            ViewCompat.stopNestedScroll(target, 1);
        }
    }
}
複製代碼

一旦知足條件,經過ViewCompat,反向調用到RecyclerView上:

public static void stopNestedScroll(@NonNull View view, @NestedScrollType int type) {
    if (view instanceof NestedScrollingChild2) {
        ((NestedScrollingChild2) view).stopNestedScroll(type);
    } else if (type == ViewCompat.TYPE_TOUCH) {
        stopNestedScroll(view);
    }
}
複製代碼

..... 總之就是層層調用,完成stop過程個最終通知。

2、CoordinatorLayout

對於CoordinatorLayout來講,已經沒有什麼好聊的了,由於RecyclerView過程當中咱們已經基本瞭解到了它的做用...

做爲一箇中間人,把NestedScrollingChild傳過來的事件,轉給Behavior,以達到將一個子View滑動的事件傳遞給另外一個子View

實戰

來作個這樣的一個效果:

原理啥的上邊都已經聊清楚了,這裏直接貼代碼(很簡單,沒幾行):

class NestedTopLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
        FrameLayout(context, attrs, defStyleAttr), NestedScrollingParent {
    private var mShowTop = false
    private var mHideTop = false
    private val mTopViewHeight = 800
    private val defaultMarginTop = 800

    override fun onFinishInflate() {
        super.onFinishInflate()
        scrollBy(0, -defaultMarginTop)
    }

    override fun onStartNestedScroll(@NonNull child: View, @NonNull target: View, nestedScrollAxes: Int): Boolean {
        return nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
    }

    override fun onNestedScrollAccepted(@NonNull child: View, @NonNull target: View, nestedScrollAxes: Int) {}
    override fun onStopNestedScroll(@NonNull target: View) {}
    override fun onNestedScroll(@NonNull target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {}

    override fun onNestedPreScroll(@NonNull target: View, dx: Int, dy: Int, @NonNull consumed: IntArray) {
        var dy = dy
        mShowTop = dy < 0 && Math.abs(scrollY) < mTopViewHeight && !target.canScrollVertically(-1)
        if (mShowTop) {
            if (Math.abs(scrollY + dy) > mTopViewHeight) {
                dy = -(mTopViewHeight - Math.abs(scrollY))
            }
        }
        mHideTop = dy > 0 && scrollY < 0
        if (mHideTop) {
            if (dy + scrollY > 0) {
                dy = -scrollY
            }
        }
        if (mShowTop || mHideTop) {
            consumed[1] = dy
            scrollBy(0, dy)
        }
    }
    
    override fun onNestedFling(@NonNull target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
        return scrollY != 0
    }

    override fun onNestedPreFling(@NonNull target: View, velocityX: Float, velocityY: Float): Boolean {
        return scrollY != 0
    }

    override fun getNestedScrollAxes(): Int {
        return ViewCompat.SCROLL_AXIS_VERTICAL
    }
}
複製代碼

尾聲

加上這篇文章,事件分發這一塊,感受已是足夠了。徹底能夠應對各類各樣的滑動體驗需求。

OK,就醬~

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是咱們在從應屆生過渡到開發這一路所踩過的坑,以及咱們一步步學習的記錄,若是感興趣的朋友能夠關注一下,一同加油~

我的公衆號:鹹魚正翻身
相關文章
相關標籤/搜索