Android Nested Scrolling

作者: kyleduo | 来源:发表于2017-03-09 23:06 被阅读190次
    covercover

    Android常规的Touch事件传递机制是自顶向下,由外向内的,一旦确定了事件消费者View,随后的事件都将传递到该View。因为是自顶向下,父控件可以随时拦截事件,下拉刷新、拖拽排序、折叠等交互效果都可以通过这套机制完成。Touch事件传递机制是Android开发必须掌握的基本内容。但是这套机制存在一个缺陷:子View无法通知父View处理事件。NestedScrolling就是为这个场景设计的。

    NestedScrollingChild和NestedScrollingParent

    NestedScrolling是指存在嵌套滚动的场景,常见于下拉刷新、展开/收起标题栏等。Support包中的CoordinatorLayoutScrollRefreshLayout就是基于NestedScrolling机制实现的。

    NestedScrollingChildNestedScrollingParent分别定义了嵌套子View和嵌套父View需要实现的接口,方法列表分别如下,可以先略过,后面会把这些方法串起来。另外这些方法基本都是通过NestedScrollingChildHelperNestedScrollingParentHelper来实现,一般并不需要手动编写多少逻辑。

    // NestedScrollingChild
    void setNestedScrollingEnabled(boolean enabled);
    boolean startNestedScroll(int axes);
    void stopNestedScroll();
    boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
    boolean hasNestedScrollingParent();
    boolean isNestedScrollingEnabled();
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
    boolean dispatchNestedPreFling(float velocityX, float velocityY);
    
    // NestedScrollingParent
    int getNestedScrollAxes();
    boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
    boolean onNestedPreFling(View target, float velocityX, float velocityY);
    void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
    void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
    boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
    void onStopNestedScroll(View target);
    

    通过方法名可以看出,NestedScrollingChild的方法均为主动方法,而NestedScrollingParent的方法基本都是回调方法。这也是NestedScrolling机制的一个体现,子View作为NestedScrolling事件传递的主动方,父View作为被动方。

    NestedScrolling机制生效的前提条件是子View作为Touch事件的消费者,在消费过程中向父View发送NestedScrolling事件(注意这里不是Touch事件,而是NestedScrolling事件)。

    NestedScrolling事件传递

    NestedScrolling机制中,NestedScrolling事件使用dx, dy表示,分别表示子View Touch事件处理方法中判定的x和y方向上的滚动偏移量。

    NestedScrolling事件的传递:

    1. 由子View产生NestedScrolling事件;
    2. 发送给父View进行处理,父View处理之后,返回消费的偏移量;
    3. 子View根据父View消费的偏移量计算NestedScrolling事件剩余偏移量;
    4. 根据剩余偏移量判断是否能处理滚动事件;如果处理滚动事件,同时将自身滚动情况通知父View;
    5. 处理结束,事件传递完成。
    1. 这里只说明了一层嵌套的情况,事实上NestedScrolling很可能出现在多重嵌套的场景。对于多重嵌套,步骤2、3、4将事件自底向上进行传递,步骤2中消费的偏移量将记录所有嵌套父View消费偏移量的总和。这里不再重复。
    2. Fling事件的传递和Scroll类似,也不再赘述。

    方法调用流程

    我们可以把上面的方法根据NestedScrolling事件传递的不同阶段进行分组(Fling跟随Scrolling发生)。

    初始阶段:确认开启NestedScrolling,关联父View和子View。

    // NestedScrollingChild
    void setNestedScrollingEnabled(boolean enabled);
    boolean startNestedScroll(int axes);
    
    // NestedScrollingParent
    int getNestedScrollAxes()
    boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
    void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
    

    预滚动阶段:子View将事件分发到父View

    // NestedScrollingChild
    boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
    
    // NestedScrollingParent
    void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    

    滚动阶段:子View处理滚动事件。

    // NestedScrollingChild
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
    
    // NestedScrollingParent
    void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
    

    结束阶段:结束。

    // NestedScrollingChild
    void stopNestedScroll();
    
    // NestedScrollingParent
    void onStopNestedScroll(View target);
    

    下面是一次嵌套滚动(三级嵌套)从开始到结束的方法调用时序图:

    methodsmethods

    金色是NestedScrollingChild的方法,为子View主动调用。

    紫色是NestedScrollingParent的回调方法,由子View相关方法调用。

    橙色为滚动事件被消费的时机

    当子View调用startNestedScroll方法时,开始嵌套滚动流程;之后不断循环pre-scroll和scroll两个过程(一般在子View的onTouchEvent的MOVE分支调用);直到手指抬起,子View调用stopNestedScroll方法结束滚动(在结束之前可能进入Fling状态)。

    划重点

    最重要的一点:pre-scroll过程是子View向父View传递事件的过程,而scroll过程才是子View消耗滚动事件的过程,也就是说父View拥有优先消费事件的权利。

    从事件消耗的优先级来看,可以画出这样一张图。

    nested_scrolling_event_flownested_scrolling_event_flow

    dispatchNestedPreScroll传给父View的是没有被消费的滚动事件,父View消费完之后通过consumed数组返回,如果还有剩余,子View进行消费,并将消费多少和剩余多少再次发给父View。

    如果一个View同时作为NestedScrollingChild和NestedScrollingParent,那么在处理onNestedPreScrolling和onNestedScrolling的时候,也要按照自底向上的规则,先让父View处理事件。

    实例分析以及Q&A

    这里通过对CoordinatorLayout -> SwipeRefreshLayout -> RecyclerView这个常用的三级嵌套实例进行分析,以便深入理解NestedScrolling事件传递的机制。

    嗯,其实上面那张时序图基本就通过方法调用的顺序,理清了传递的过程。

    这里通过几个Q&A,来解答疑惑。

    如果你还不清楚SwipeRefreshLayout的原理,建议先去看一下我的另一篇文章:SwipeRefreshLayout源码分析

    CL代表CoordinatorLayout,SRL代表SwipeRefreshLayout,RV表示RecyclerView。实在打不动字了……

    Q1: SwipeRefreshLayout在Touch事件分发过程中,为什么SwipeRefreshLayout没有作为Touch事件的消费者?

    A1: Touch事件流从ACTION_DOWN开始:

    1. 先经过SRL的onInterceptTouchEvent(),返回false
    2. 进入RV的onInterceptTouchEvent(),进入ACTION_DOWN分支,RV调用startNestedScrolling()方法。
    3. 根据上面的时序图,会调用SRL的onNestedScrollAccepted(),而这个方法里面,会将SRL的mNestedScrollInProgress设置为true。事实上到此为止已经进入了NestedScrolling事件的分发流程。
    4. 后续事件,SRL的onInterceptTouchEvent()方法会根据mNestedScrollInProgress属性返回false,也就不会拦截事件了。
    5. CV的部分根据时序图可以清楚理解。
    Q2: 接Q1,既然没有拦截,为什么还能处理事件?

    A2: 首先,要注意SRL处理的不是Touch事件,而是NestedScrolling事件,还记得吗,实际上是以(dx, dy)偏移量的形式存在的。A1中可以看到,一旦触发NestedScrolling机制,作为父View的SRL,就有优先处理NestedScrolling事件的权利,所以当然能处理事件(当然优先级比CL低,所以只能处理CL处理剩下的部分)。

    Q3: 为什么CL能消费事件进行滚动?

    **A3: **NestedScrolling机制决定NestedScrolling事件时自底向上传播的,并且通过pre-scroll和scroll两个过程的划分,越上层的View,处理NestedScrolling事件的优先级越高。这个例子中,CL在最上层,自然优先处理事件。

    Q4: 对于SwipeRefreshLayout来说,什么时候通过onTouchEvent方法处理事件,什么时候通过NestedScrolling机制处理事件?

    A4: NestedScrolling机制由实现了NestedScrollingChild接口的子View触发,所以事实上,当SRL的子View实现了NestedScrollingChild接口时,均会使用NestedScrolling机制分发事件给SRL。比如RecyclerView作为子View将通过NestedScrolling处理事件,如果是ListView作为子View,将通过Touch机制处理事件。

    总结

    读到这里你会发现,要理解NestedScrolling,实际上就是要理解NestedScrolling事件分发流程。这篇博客写了两个晚上,很久没有花这么长时间写huatu客了,希望能给你带来帮助。欢迎转发分享赞赏。

    相关文章

      网友评论

      • suikaJY:写的很棒,请问您的时序图是用什么画的呢?
        kyleduo:@suikaJY Sketch

      本文标题:Android Nested Scrolling

      本文链接:https://www.haomeiwen.com/subject/rrnggttx.html