Android Nested Scrolling

cover
cover

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容