现在很多大厂App首页都是上面自定义布局(分类、广告)+ tab + viewpager(RecyclerView)
要实现这样的效果,主要问题就是解决:事件分发、滑动冲突、嵌套滑动。 这三个问题。
没处理前的效果
问题:
1.滑上面根本划不动
2.滑下面RecyclerView滑了,但是上面没跟着动
3.没有吸顶效果
初始效果
问题分析
问题1上方无法滑动,是因为没有使用嵌套滑动,即使外层用ScrollView包裹,但还是没效果。
解决:
点进去RecyclerView的源码,发现他不仅实现了ScrollingView,还实现了NestedScrollingChild2
那么NestedScrollingChild2这玩意儿是啥呢?其实是谷歌给我们实现的NestedScrolling嵌套滑动,既然有child儿子,那么肯定得有父亲,所以外层滚动的ScrollingView得换成NestedScrollView。
NestedScrollView这个View其实即是儿子又是父亲
但是即使换上了NestedScrollView效果还是一样的(有些版本是能划的),我估计是下层的ViewPager(实现RecyclerView的Fragment)没有高度所致。
问题3吸顶:紧接上面问题1尾,得设置ViewPager高度,偷懒的吸顶做法,将tab + RecyclerView = 整个屏幕的高度(父控件的全部高度),这样不就是吸顶了么。
其实是一个假吸顶,因为滑到顶了 划不动了。
吸顶的其他实现:
1.计算滑动的距离和头的高度,然后判断是不是该停了,如果该停了那就在设置宽高
2.上面做一个隐藏的tab,当移动tab移到头部,将隐藏的tab显示
3.重写谷歌的CoordinatorLayout
4.等等
问题2事件被子View消费:下面滑了,上面没跟着动,是因为事件分发,事件给子布局RecyclerView消费了,上面的NestedScrolling没有拦截处理,那就得具体看事件分发的源码了。
问题解决1
先解决简单的,把tab和RecyclerView的高度设置了
自定义NestedScrollView,继承NestedScrollView,然后用一个布局将tab和RecyclerView包裹,取到那个LinearLayout,然后将高度设置成父控件的高度
onFinishInflate:XML布局被加载完后,就会回调onFinshInfalte这个方法,在这个方法中我们可以初始化控件和数据。
在onMeasure里设置高度
效果1:这时候上面能划了,也有吸顶了,但是还差滑动RecyclerView的时候将事件给父布局先划,父亲划完了儿子再划
事件分发
事件
所谓的事件,也就是手指点击屏幕的事件。
在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…
- 事件分发的起点,肯定是Activity,叫dispatchTouchEvent()这个方法(翻译调度触摸事件)
- Activity会将事件给getWindow().superDispatchTouchEvent(ev),这里的window就是PhoneWindow(详情看juejin.cn/post/697392… )
PhoneWindow又将事件分发给他的下层DecorView的superDispatchTouchEvent
- DecorView又是调用super.dispatchTouchEvent(event),DecorView他的爹就是GroupView,所以是调到GroupView中的dispatchTouchEvent
- GroupView中的dispatchTouchEvent是重点方法
GroupView 一般都是父亲,所以他的dispatchTouchEvent需要处理 ,事件是否由我自己处理或者下发到哪一个View给他处理
4.1 开始先判断处理一些辅助功能、是否处理了、安全性判断等。都没问题走到下面。
①如果是down按下(事件的开始),那就把上回的事件、变量等清空(touchTarget链表)
mFirstTouchTarget触摸目标链接列表中的第一个触摸目标。也就是子View的触摸列表,如果为空那就是点到空白的地方了,ViewGroup自己处理
②判断拦截情况。是否是down或者mFirstTouchTarget不为空(也就是是不是点到空白地方,点空白地方自己处理,不然就可能下发给子View处理)
③子类是否设置了父亲不能拦截
④正常都走onInterceptTouchEvent()方法,这个方法就是判断下不下发的,true那就自己处理,false就下发
③补充:通过搜索FLAG_DISALLOW_INTERCEPT,查到一个方法,这个方法就是可以设置,不让父亲拦截自己的事件
requestDisallowInterceptTouchEvent()
⑤if (!canceled && !intercepted),那就遍历儿子,去找具体下发谁
⑤补充 如果是事件起点才遍历
⑥通过dispatchTransformedTouchEvent去调用child.dispatchTouchEvent()尝试去消费该事件
⑦如果返回ture说明儿子处理了,所以得记录到mFirstTouchTarget这个链表上
补充:
⑧这样就处理完了,分发给儿子那就调用的是儿子的onTouchEvent,如果是自己处理的,那就调用的自己的onTouchEvent
事件分发总结
嵌套滑动
嵌套滑动的生命周期可以通过继承NestedScrollView,然后把所有的相关方法都Log打印出来,来查看学习。
NestedScrollView生命周期主要分三块:
1.initialize(初始化)确定儿子滚动了
2.onTouchEvent儿子滚动
3.fling滚动完事了,因为还有惯性,所以还能滚动一些距离(优化这一块)
问题解决2
重点是第8步,儿子滚动之前会先调父亲的onNestedPreScroll,我要滚动了
onNestedPreScroll该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2
现在已经能用了,但是划上面没有惯性,还能小优化一下
问题解决3 – 惯性
RecyclerView的滑动的惯性,是由一个方法决定的:
public boolean fling(int velocityX, int velocityY)
这个方法入参表示x的速度,y的速度。
所以问题就在具体的惯性的速度是多少
分析:我知道初始手指滑动的速度。那就是
初始滑动的速度–>通过某算法计算得到应该需要滑动的距离。
应该要滑动的距离 – 已经滑动的距离 = 剩下惯性滑动的距离。
剩下惯性滑动的距离 –> 通过那个算法反推回速度。
这个惯性滑动距离的算法谷歌给了,看不懂,用就完事了
算出惯性速度,传入childRecyclerView.fling(0, velY);
最后还发现下面很丝滑,但是划头部RecyclerView的时候有点僵硬,原因是RecyclerView将滑动事件吃了,得设置头部RecyclerView不可滑动,所以自定义RecyclerView,然后重写onTouchEvent、onInterceptTouchEvent
完事,真的有那么丝滑吗? --- 德芙