Android嵌套滾動機制

一 什麼是嵌套滾動

當父容器和子控件都支持滾動時的一種協做機制.android

二 嵌套滾動的應用場景

1.應對事件分發機制的侷限性:
父容器一旦攔截事件本身處理,後續事件不能再傳遞給子控件消費.markdown

2.事件衝突的一種解決方案oop

2.1 CoordinatorLayout配合RecyclerView|NestedScrollView

先滾動父容器再滾動子控件:post

2.2 swiperefreshlayout

處理手勢衝突:this

2.3 bottomSheetDialog

處理手勢衝突:spa

三 嵌套滾動原理解析

嵌套滾動機制從android5.0中加入,5.0之前的兼容方案由四個類支持:code

接口類:
NestedScrollingChild
NestedScrollingParent
實現類:
NestedScrollingChildHelper
NestedScrollingParentHelperorm

sequenceDiagram
NestedScrollingChild->>NestedScrollingParent: 子控件觸發嵌套滾動:startNestedScroll()
NestedScrollingParent->>NestedScrollingChild: onStartNestedScroll()
loop 1-n個滾動事件
Note over NestedScrollingParent,NestedScrollingChild: 1.父容器優先消費
NestedScrollingChild->>NestedScrollingParent: dispatchNestedPreScroll()
NestedScrollingParent->>NestedScrollingChild: onNestedPreScroll()
Note over NestedScrollingParent,NestedScrollingChild: 2.子控件消費滾動
NestedScrollingChild->>NestedScrollingParent: dispatchNestedScroll()
NestedScrollingParent->>NestedScrollingChild: onNestedScroll()
Note over NestedScrollingParent,NestedScrollingChild: 3.父容器處理未被消費的滾動距離
end
rect rgb(0, 255, 255)
opt 0-1個慣性滾動
NestedScrollingChild-->>NestedScrollingParent: dispatchNestedPreFling()
NestedScrollingParent-->>NestedScrollingChild: onNestedPreFling()
NestedScrollingChild-->>NestedScrollingParent: dispatchNestedFling()
NestedScrollingParent-->>NestedScrollingChild: onNestedFling()
end
end
NestedScrollingChild-->>NestedScrollingParent: 子控件結束嵌套滾動:stopNestedScroll()
NestedScrollingParent->>NestedScrollingChild: onStopNestedScroll()

NestedScrollingChild類圖接口

classDiagram
NestedScrollingChild <|-- NestedScrollingChild2
NestedScrollingChild2 <|-- NestedScrollingChild3
NestedScrollingChild2 <|.. RecyclerView
NestedScrollingChild3 <|.. RecyclerView
NestedScrollingChild3 <|.. NestedScrollView
<<interface>> NestedScrollingChild
class NestedScrollingChild{
+setNestedScrollingEnabled(boolean)
+isNestedScrollingEnabled()
+startNestedScroll(int)
+stopNestedScroll()
+hasNestedScrollingParent()
+dispatchNestedScroll(int, int,int, int, int[])
+dispatchNestedPreScroll(int, int, int[], int[])
+dispatchNestedFling(float, float, boolean)
+dispatchNestedPreFling(float, float)
}
<<interface>> NestedScrollingChild2
class NestedScrollingChild2{
+startNestedScroll(int,int)
+stopNestedScroll(int)
+hasNestedScrollingParent(int)
+dispatchNestedScroll(int, int,int, int, int[],int)
+dispatchNestedPreScroll(int, int, int[], int[],int)
}
<<interface>> NestedScrollingChild3
class NestedScrollingChild3{
+dispatchNestedScroll(int, int,int, int, int[],int,int[])
}
class RecyclerView
class NestedScrollView

NestedScrollingParent類圖事件

classDiagram
NestedScrollingParent <|-- NestedScrollingParent2
NestedScrollingParent2 <|-- NestedScrollingParent3
NestedScrollingParent2 <|.. CoordinatorLayout
NestedScrollingParent3 <|.. CoordinatorLayout
NestedScrollingParent3 <|.. NestedScrollView
NestedScrollingParent3 <|.. MotionLayout
<<interface>> NestedScrollingParent
class NestedScrollingParent{
+onStartNestedScroll(View,View, @ScrollAxis int)
+onNestedScrollAccepted(View,View, @ScrollAxis int)
+onStopNestedScroll(View)
+onNestedScroll(View, int, int, int, int)
+onNestedPreScroll(View, int, int,int[])
+onNestedFling(View, float, float, boolean)
+onNestedPreFling(View, float, float)
+getNestedScrollAxes()
}
<<interface>> NestedScrollingParent2
class NestedScrollingParent2{
+onStartNestedScroll(View,View, @ScrollAxis int, @NestedScrollType int)
+onNestedScrollAccepted(View,View, @ScrollAxis int, @NestedScrollType int)
+onStopNestedScroll(View, @NestedScrollType int)
+onNestedScroll(View, int, int, int, int, @NestedScrollType int)
+onNestedPreScroll(View, int, int,int[], @NestedScrollType int)
}
<<interface>> NestedScrollingParent3
class NestedScrollingParent3{
+onNestedScroll(View, int, int, int, int, @NestedScrollType int,int[])
}
class CoordinatorLayout
class NestedScrollView

NestedScrollingChildHelper類圖

classDiagram
class NestedScrollingChildHelper{
+NestedScrollingChildHelper(View)
+setNestedScrollingEnabled(boolean)
+isNestedScrollingEnabled()
+startNestedScroll(int)
+stopNestedScroll()
+hasNestedScrollingParent()
+dispatchNestedScroll(int, int,int, int, int[])
+dispatchNestedPreScroll(int, int, int[], int[])
+dispatchNestedFling(float, float, boolean)
+dispatchNestedPreFling(float, float)
+onDetachedFromWindow()
+onStopNestedScroll(View)
}

NestedScrollingParentHelper類圖

classDiagram
class NestedScrollingParentHelper{
+NestedScrollingParentHelper(ViewGroup)
+onNestedScrollAccepted(View,View, @ScrollAxis int)
+onStopNestedScroll(View)
+getNestedScrollAxes()
}

5.0之後嵌套滾動的實現被寫進了ViewViewGroup裏:

sequenceDiagram
View->>ViewGroup: 子控件觸發嵌套滾動:startNestedScroll()
ViewGroup->>View: onStartNestedScroll()
loop 1-n個滾動事件
Note over ViewGroup,View: 1.父容器優先消費
View->>ViewGroup: dispatchNestedPreScroll()
ViewGroup->>View: onNestedPreScroll()
Note over ViewGroup,View: 2.子控件消費滾動
View->>ViewGroup: dispatchNestedScroll()
ViewGroup->>View: onNestedScroll()
Note over ViewGroup,View: 3.父容器處理未被消費的滾動距離
end
rect rgb(0, 255, 255)
opt 0-1個慣性滾動
View-->>ViewGroup: dispatchNestedPreFling()
ViewGroup-->>View: onNestedPreFling()
View-->>ViewGroup: dispatchNestedFling()
ViewGroup-->>View: onNestedFling()
end
end
View-->>ViewGroup: 子控件結束嵌套滾動:stopNestedScroll()
ViewGroup->>View: onStopNestedScroll()

ViewViewGroup中的嵌套滾動相關方法:

classDiagram
View <|-- ViewGroup
class View{
+setNestedScrollingEnabled(boolean)
+isNestedScrollingEnabled()
+startNestedScroll(int)
+stopNestedScroll()
+hasNestedScrollingParent()
+dispatchNestedScroll(int, int,int, int, int[])
+dispatchNestedPreScroll(int, int, int[], int[])
+dispatchNestedFling(float, float, boolean)
+dispatchNestedPreFling(float, float)
}
class ViewGroup{
+onStartNestedScroll(View,View, @ScrollAxis int)
+onNestedScrollAccepted(View,View, @ScrollAxis int)
+onStopNestedScroll(View)
+onNestedScroll(View, int, int, int, int)
+onNestedPreScroll(View, int, int,int[])
+onNestedFling(View, float, float, boolean)
+onNestedPreFling(View, float, float)
+getNestedScrollAxes()
}

嵌套滾動的觸發和中止

開始嵌套滾動:

NestedScrollingChild.startNestedScroll(int axes)
NestedScrollingChild.stopNestedScroll()
複製代碼

中止嵌套滾動:

NestedScrollingChild.setNestedScrollingEnabled(false)
NestedScrollingChild.stopNestedScroll()
複製代碼

嵌套滾動和事件分發機制的關係(以RecyclerView爲例)

public boolean onTouchEvent(MotionEvent e) {
    ...
    switch (action) {
    case MotionEvent.ACTION_DOWN: {
    ...
    //1.按下時開始嵌套滾動
    startNestedScroll(nestedScrollAxis, TYPE_TOUCH); } 
    break;
    
    case MotionEvent.ACTION_MOVE: {
    ...
    //2.移動時給父容器優先消費
    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];
    ...
    //3.內部通過scrollByInternal()實現滾動,而後將已消費的距離和未消費的距離傳遞給父控件處理
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
    ...
    }
    }break;
    case MotionEvent.ACTION_UP: {
    ...
    //4.內部執行fling()方法優先給父容器執行慣性滑動的機會
    if (!dispatchNestedPreFling(velocityX, velocityY)) { 
    ...
    //5.父控件不消費慣性滾動,子控件自行處理.給父容器觀察子控件fling事件的機會
    dispatchNestedFling(velocityX, velocityY, canScroll); 
    ...
    mViewFlinger.fling(velocityX, velocityY);  
    //內部經過resetTouch()方法結束嵌套滾動
    stopNestedScroll(TYPE_TOUCH);
    }
    }break;
}
複製代碼

嵌套fling的版本差別性

support庫v25之前:fling事件不支持部分消費.父容器要麼攔截整個fling事件,要麼交給子控件處理.

support庫v26之後:新增了父容器部分消費fling事件的支持

核心實現是NestedScrollingChild2NestedScrollingParent2這兩個類,重載了嵌套滾動相關方法,增長了type參數.以前的nestedscroll只有手動觸發一種,如今有兩種嵌套滾動:
手動觸發-TYPE_TOUCH=0
代碼觸發-TYPE_NON_TOUCH=1
nestedfling就被轉換成了TYPE_NON_TOUCH類型的nestedscroll,間接的支持了嵌套fling的部分消費.

NestedScrollView爲例:

fling方法開始代碼觸發的嵌套滾動:

public void fling(int velocityY) {
    if (this.getChildCount() > 0) {
        this.mScroller.fling(this.getScrollX(), this.getScrollY(), 0, velocityY, 0, 0, -2147483648, 2147483647, 0, 0);
        this.runAnimatedScroll(true);
    }

}

private void runAnimatedScroll(boolean participateInNestedScrolling) {
    if (participateInNestedScrolling) {
        //第二個參數是type,1對應TYPE_NON_TOUCH
        this.startNestedScroll(2, 1);
    } else {
        this.stopNestedScroll(1);
    }

    this.mLastScrollerY = this.getScrollY();
    ViewCompat.postInvalidateOnAnimation(this);
}
複製代碼

在computeScroll方法中調用dispatchNestedPreScroll()和dispatchNestedScroll()方法:

public void computeScroll() {
    if (!this.mScroller.isFinished()) {
        ...
        //1.給父容器優先消費
        this.dispatchNestedPreScroll(0, unconsumed, this.mScrollConsumed, (int[])null, 1);
        //子控件可滾動的距離=總距離-父容器消費的部分
        unconsumed -= this.mScrollConsumed[1];
        ...
        if (unconsumed != 0) {
            mode = this.getScrollY();
            //2.執行子控件自身的滾動
            this.overScrollByCompat(0, unconsumed, this.getScrollX(), mode, 0, range, 0, 0, false);
            ...
            //3.將已消費的距離和未消費的距離傳遞給父容器處理
            this.dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, this.mScrollOffset, 1, this.mScrollConsumed);
            ...
        }

        ...

        if (!this.mScroller.isFinished()) {
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            //4.滾性滾動結束時中止代碼觸發的嵌套滾動
            this.stopNestedScroll(1);
        }

    }
}
複製代碼
相關文章
相關標籤/搜索