Android Nested Scrolling

2,079 阅读6分钟
原文链接: blog.kyleduo.com

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

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

methods

金色是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_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客了,希望能给你带来帮助。欢迎转发分享。