事件分发浅析 - 仿京东首页二级联动

2,909 阅读6分钟

现在很多大厂App首页都是上面自定义布局(分类、广告)+ tab + viewpager(RecyclerView)

444.gif

要实现这样的效果,主要问题就是解决:事件分发、滑动冲突、嵌套滑动。 这三个问题。
image.png

没处理前的效果

问题:
1.滑上面根本划不动
2.滑下面RecyclerView滑了,但是上面没跟着动
3.没有吸顶效果

初始效果
aaa.gif

问题分析

问题1上方无法滑动,是因为没有使用嵌套滑动,即使外层用ScrollView包裹,但还是没效果。
解决:
点进去RecyclerView的源码,发现他不仅实现了ScrollingView,还实现了NestedScrollingChild2 image.png

那么NestedScrollingChild2这玩意儿是啥呢?其实是谷歌给我们实现的NestedScrolling嵌套滑动,既然有child儿子,那么肯定得有父亲,所以外层滚动的ScrollingView得换成NestedScrollView。

NestedScrollView这个View其实即是儿子又是父亲 image.png

但是即使换上了NestedScrollView效果还是一样的(有些版本是能划的),我估计是下层的ViewPager(实现RecyclerView的Fragment)没有高度所致。

问题3吸顶:紧接上面问题1尾,得设置ViewPager高度,偷懒的吸顶做法,将tab + RecyclerView = 整个屏幕的高度(父控件的全部高度),这样不就是吸顶了么。
其实是一个假吸顶,因为滑到顶了 划不动了。
吸顶的其他实现:
1.计算滑动的距离和头的高度,然后判断是不是该停了,如果该停了那就在设置宽高
2.上面做一个隐藏的tab,当移动tab移到头部,将隐藏的tab显示
3.重写谷歌的CoordinatorLayout
4.等等

image.png

问题2事件被子View消费:下面滑了,上面没跟着动,是因为事件分发,事件给子布局RecyclerView消费了,上面的NestedScrolling没有拦截处理,那就得具体看事件分发的源码了。

问题解决1

先解决简单的,把tab和RecyclerView的高度设置了

自定义NestedScrollView,继承NestedScrollView,然后用一个布局将tab和RecyclerView包裹,取到那个LinearLayout,然后将高度设置成父控件的高度

image.png

onFinishInflate:XML布局被加载完后,就会回调onFinshInfalte这个方法,在这个方法中我们可以初始化控件和数据。
image.png

在onMeasure里设置高度
image.png

效果1:这时候上面能划了,也有吸顶了,但是还差滑动RecyclerView的时候将事件给父布局先划,父亲划完了儿子再划

111.gif

事件分发

事件

所谓的事件,也就是手指点击屏幕的事件。
在android里,事件就是MotionEvent对象
分为单点触摸、多点触摸、手势,三种。

单点触摸:ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL(cancel取消,被上层拦截),事件动ACTION_DOWN手指按下开始,ACTION_UP手指抬起结束,中间有0~n个ACTION_MOVE手指移动

多点触摸:ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL除了单点触摸的4个还有ACTION_POINTER_DOWN、ACTION_POINTER_UP。
第一个手指按下ACTION_DOWN,事件开始;
第二个第三个...按下ACTION_POINTER_DOWN;
手指抬起,前面手指抬起全是ACTION_POINTER_UP;
最后一个手指抬起ACTION_UP,事件结束
(android最多支持32个触摸点)

事件分发源码

Activity布局结构这样的:
具体:juejin.cn/post/697392…

image.png

  1. 事件分发的起点,肯定是Activity,叫dispatchTouchEvent()这个方法(翻译调度触摸事件)

image.png

  1. Activity会将事件给getWindow().superDispatchTouchEvent(ev),这里的window就是PhoneWindow(详情看juejin.cn/post/697392…

PhoneWindow又将事件分发给他的下层DecorView的superDispatchTouchEvent

image.png

  1. DecorView又是调用super.dispatchTouchEvent(event),DecorView他的爹就是GroupView,所以是调到GroupView中的dispatchTouchEvent

image.png

  1. GroupView中的dispatchTouchEvent是重点方法

GroupView 一般都是父亲,所以他的dispatchTouchEvent需要处理 ,事件是否由我自己处理或者下发到哪一个View给他处理

4.1 开始先判断处理一些辅助功能、是否处理了、安全性判断等。都没问题走到下面。

①如果是down按下(事件的开始),那就把上回的事件、变量等清空(touchTarget链表) image.png

mFirstTouchTarget触摸目标链接列表中的第一个触摸目标。也就是子View的触摸列表,如果为空那就是点到空白的地方了,ViewGroup自己处理

②判断拦截情况。是否是down或者mFirstTouchTarget不为空(也就是是不是点到空白地方,点空白地方自己处理,不然就可能下发给子View处理)
③子类是否设置了父亲不能拦截
④正常都走onInterceptTouchEvent()方法,这个方法就是判断下不下发的,true那就自己处理,false就下发 image.png

③补充:通过搜索FLAG_DISALLOW_INTERCEPT,查到一个方法,这个方法就是可以设置,不让父亲拦截自己的事件
requestDisallowInterceptTouchEvent() image.png

⑤if (!canceled && !intercepted),那就遍历儿子,去找具体下发谁 image.png

⑤补充 如果是事件起点才遍历 image.png

⑥通过dispatchTransformedTouchEvent去调用child.dispatchTouchEvent()尝试去消费该事件
⑦如果返回ture说明儿子处理了,所以得记录到mFirstTouchTarget这个链表上

image.png

补充:

image.png image.png

⑧这样就处理完了,分发给儿子那就调用的是儿子的onTouchEvent,如果是自己处理的,那就调用的自己的onTouchEvent

事件分发总结

PO.png
image.png

嵌套滑动

嵌套滑动的生命周期可以通过继承NestedScrollView,然后把所有的相关方法都Log打印出来,来查看学习。

NestedScrollView生命周期主要分三块:
1.initialize(初始化)确定儿子滚动了
2.onTouchEvent儿子滚动
3.fling滚动完事了,因为还有惯性,所以还能滚动一些距离(优化这一块) image.png

问题解决2

重点是第8步,儿子滚动之前会先调父亲的onNestedPreScroll,我要滚动了
onNestedPreScroll该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2

image.png

现在已经能用了,但是划上面没有惯性,还能小优化一下
C5F5B600.gif

问题解决3 - 惯性

RecyclerView的滑动的惯性,是由一个方法决定的:
public boolean fling(int velocityX, int velocityY) 这个方法入参表示x的速度,y的速度。

所以问题就在具体的惯性的速度是多少 image.png

分析:我知道初始手指滑动的速度。那就是
初始滑动的速度-->通过某算法计算得到应该需要滑动的距离。
应该要滑动的距离 - 已经滑动的距离 = 剩下惯性滑动的距离。
剩下惯性滑动的距离 --> 通过那个算法反推回速度。

这个惯性滑动距离的算法谷歌给了,看不懂,用就完事了
image.png

算出惯性速度,传入childRecyclerView.fling(0, velY); image.png

最后还发现下面很丝滑,但是划头部RecyclerView的时候有点僵硬,原因是RecyclerView将滑动事件吃了,得设置头部RecyclerView不可滑动,所以自定义RecyclerView,然后重写onTouchEvent、onInterceptTouchEvent

image.png

完事,真的有那么丝滑吗? --- 德芙

C0CE14DFEE20BF278DDCC353EE29FF1D.gif

事件分发源码详解(也就是这篇的下)
juejin.cn/post/697614…