阅读 1177

Android嵌套滚动机制

一 什么是嵌套滚动

当父容器和子控件都支持滚动时的一种协作机制.

二 嵌套滚动的应用场景

1.应对事件分发机制的局限性:
父容器一旦拦截事件自己处理,后续事件不能再传递给子控件消费.

2.事件冲突的一种解决方案

2.1 CoordinatorLayout配合RecyclerView|NestedScrollView

先滚动父容器再滚动子控件:

2.2 swiperefreshlayout

处理手势冲突:

2.3 bottomSheetDialog

处理手势冲突:

三 嵌套滚动原理解析

嵌套滚动机制从android5.0中加入,5.0以前的兼容方案由四个类支持:

接口类:
NestedScrollingChild
NestedScrollingParent
实现类:
NestedScrollingChildHelper
NestedScrollingParentHelper

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

    }
}
复制代码
文章分类
Android
文章标签