NestedScroll机制


所谓嵌套滚动,是针对View与ViewGroup来说的,即二者联动。
当我们手指在一个View上滑动一定delta距离时,通过scrollTo让View实现delta距离的滚动,触屏事件完全消耗在View上面,父View无法消费滑动事件。为了让View与父View同时对某一滑屏距离做出滑动反应,View和ViewGroup的交互接口ViewParent包含NestedScroll相关方法,于是就有了嵌套滚动这个东西。

View中NestScroll方法如图所示。
NestedScroll机制_第1张图片
NestScroll_View.png

ViewGroup实现ViewParent接口中NestScroll方法如图所示。
NestedScroll机制_第2张图片
NestScroll_ViewGroup.png
嵌套滑动的核心本质有两种情况。

一种是父View主动型,子View在消费事件滑动的距离时,先问问父View,父View主动决定自己消耗掉多少距离,然后给子View留多少距离。
另一种是子View主动型,子View先消耗,然后将消耗与未消耗的距离一起告诉父View。

注意一点,嵌套滚动父视图的方法都是onXxx开头的,有的视图既可以在嵌套滚动中作为子视图,也能作为父视图。


查找可接受嵌套滚动的父视图

View开始准备NestScroll嵌套滚动时,触发View#startNestedScroll方法。
View#startNestedScroll方法。

public boolean startNestedScroll(int axes) {
    //已经存在mNestedScrollingParent的节点,直接返回
    if (hasNestedScrollingParent()) {
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = getParent();
        View child = this;
        while (p != null) {
            try {
                if (p.onStartNestedScroll(child, this, axes)) {
                    mNestedScrollingParent = p;
                    p.onNestedScrollAccepted(child, this, axes);
                    return true;
                }
            } catch (AbstractMethodError e) {
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}
  • hasNestedScrollingParent方法,View内部的mNestedScrollingParent,如果已经初始化过,直接返回true。否则,需要向上找到一个配合嵌套滚动的父视图。
  • View支持嵌套滚动时,isNestedScrollingEnabled方法是true,如果一个View支持嵌套滚动,提前通过setNestedScrollingEnabled方法设置mPrivateFlags3标志位。
  • 在树视图结构中,向上一层层查找ViewParent父节点,调用ViewParent#onStartNestedScroll方法,父视图支持嵌套滚动的条件是onStartNestedScroll方法返回true,ViewGroup#onStartNestedScroll默认返回值false。因此,支持NestScroll的父容器需重写onStartNestedScroll方法。
  • 如果没有父视图的onStartNestedScroll方法是true,说明均不支持嵌套滚动。

ViewGroup#onStartNestedScroll方法

@Override
public boolean onStartNestedScroll(View child, View target,  
                        int nestedScrollAxes) {
    return false;
}
  • 重写ViewGroup#onStartNestedScroll方法,父容器接受嵌套滚动,将View的mNestedScrollingParent设为该父容器。
  • ViewGroup#onNestedScrollAccepted方法设置axes滚动轴。

父视图的onStartNestedScroll方法需要重写,返回true,才会支持配合子视图嵌套滚动,同时,赋值子视图内部mNestedScrollingParent。
通过startNestedScroll验证同步View与父View支持NestedScroll后,下面开启滚动。


子视图dispatchNestedPreScroll方法

View#dispatchNestedPreScroll,把自己的滚动机会先让给父View
可以在子视图触摸事件中调用,该方法在基类View。

View#dispatchNestedPreScroll方法。

public boolean dispatchNestedPreScroll(int dx, int dy,
                    int[] consumed,  int[] offsetInWindow) {
    //执行条件是本视图是支持嵌套滚动且内部支持的父视图不空。
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        if (dx != 0 || dy != 0) {
            int startX = 0;
            int startY = 0;
            ...
            //先初始化为0,
            consumed[0] = 0;
            consumed[1] = 0;
            mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);
            ...
            return consumed[0] != 0 || consumed[1] != 0;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}

父View触发onNestedPreScroll方法时,通过父View的scrollBy方法移动父View的内部视图,将移动的(即消耗掉)的距离存储在consumed中,通过consumed数组通知子View,我已经消耗掉你应该移动的距离了,你自己就别动了,跟我一起动吧,这时处理的是子View的onTouch事件。

父View先消耗,子View紧跟着父View移动,未消耗掉的子View继续消耗,让子View与父View联动。
父View重写onNestedPreScroll方法。ViewGroup#onNestedPreScroll方法默认什么都不做,最近发现23版本的ViewGroup在该方法什么都没做,而27版本的默认会触发dispatchNestedPreScroll方法,意思就是,如果父视图也是可以嵌套滚动,去找他自己的父视图去消费哈,如果上层没有支持的父视图,也什么都不做了。


子视图dispatchNestedScroll方法

View#dispatchNestedScroll,把自己的滚动机会先让给自己。

View#dispatchNestedScroll。

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, 
                  int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        if (dxConsumed != 0 || dyConsumed != 0 || 
                        dxUnconsumed != 0 || dyUnconsumed != 0) {
            int startX = 0;
            int startY = 0;
            ...
            mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
            ...
            return true;
        } else if (offsetInWindow != null) {
            offsetInWindow[0] = 0;
            offsetInWindow[1] = 0;
        }
    }
    return false;
}

父View触发onNestedScroll方法时,子View主动将消费的距离与未消费的距离通知父View。

父View重写onNestedScroll方法。ViewGroup#onNestedScroll方法默认什么都不做。
当父View收到子View未能消费的距离后,在父View的坐标下完成剩余偏移。这种场景是子View主动先消费,剩下的交给父View。

总结一下

注意ViewGroup的onNestedScroll方法和onNestedPreScroll方法,他们都是去消费子视图的移动距离。
dispatchNestedPreScroll和dispatchNestedScroll的区别,前者把机会先让给父视图,后者把机会先留给自己。


任重而道远

你可能感兴趣的:(NestedScroll机制)