首先让我们看一个效果:
在很早之前, 我们想实现上面这个效果的通常做法是自己写一个ViewGroup, 拦截下触摸事件, 控制里面滑动事件的分发. 如果第一个view已经滑出屏幕,则把剩下的事件交给recyclerview处理.
处理过的同学们都知道, 分发复杂,还要考虑fling这种操作怎么处理,做起来踩坑不断. 好在Android在之后的版本推出了很多替代的方案: 首先看到这个布局, 首选是用CoordinatorLayout来处理, 官方提供的十分好用,不太熟悉的同学可以查一下. 如果说我不想用CoordinatoLayout,想自己来解决嵌套滑动可不可以呢?答案是肯定的, 谷歌为我们提供了下面两个接口:
NestedScrollingParent(这个接口负责处理嵌套滑动)
NestedScrollingChild(这个接口负责将滑动事件分发给实现了NestedScrollingParent接口的类)
这里我们先看一下接口中都有哪些方法,首先是NestedScrollingParent:
然后是NestedScrollingChild:
初次看到这么多接口肯定都一脸蒙蔽. 没关系,我们先暂时将接口放一边, 来想一下如果要你来设计一个解决嵌套滑动的框架,要怎么实现. emmm, 首先嵌套滑动肯定是因为一个同方向的滑动, 有两个或者以上的view关心, 然后我们根据消费这个事件的优先级, 来分配这个滑动事件.(如例子中, 我们先把上滑的事件给第一个子view,待其不想继续消费时,即滑到第二部分时,再把事件分发给recyclerview). 按照这个思路, 我们就需要有一个能够将生成出的滑动事件抛出去的类,同时要有一个地方能够接收到这些滑动事件,并且决定怎么分配(消费).
好了,现在让我们回头看一下上面两个接口,NestedScrollingChild正是上面说的将滑动事件抛出去的类, 所有有可能产生滑动事件的类(如recyclerview、viewpager)都应该继承这个接口,在滑动事件发生时,自己先不处理,抛出去给其他有可能消费这个事件的类(NestedScrollingParent)去处理.然后就很好理解了,触摸事件经由NestedScrollingChild传到我这里, 我来决定消不消费(onStartNestedScroll),消费多少(onNestedPreScroll、onNestedPreFling),怎么消费, 这就是NestedScrollingParent这个接口需要处理的问题. 怎么样,上面这一套机制是不是很熟悉,是不是和触摸事件分发有点像.
如果你细心看官方文档的话会发现,除了上面两个接口, 谷歌还提供了NestedScrollingChild2、NestedScrollingParent2以及NestedScrollingChild3、NestedScrollingParent3这两对接口,这是怎么回事呢?
其实这是嵌套滑动处理不断优化的结果,如果你使用过第一代NestedScrollingParent接口就会发现,在处理fling事件的时候,如果第一个view没有把fling的距离和速度消费完,剩下的fling事件也没有机会传回给child了,为了兼容的解决这个问题,谷歌的工程师们想到了一个方法,就是把fling事件分多次作为scroll事件分发,通过type区分
从上图中可以看到, 所有和fling有关的接口都消失了,取而代之的是各个接口的最后多了一个type参数,参数的取值其实就是表示是scroll还是fling. NestedScrollingParent3这个接口, 这里卖个关子暂不介绍, 有兴趣的同学可以作为扩展阅读,查一下官方的文档和源码, 体会一下谷歌的接口优化方案.
上面说了一大堆滑动嵌套相关,下面回到开头的demo,如果要实现一个这样的布局要怎么做呢? 相信聪明的你已经有了思路. 没错,其实只要我们实现NestedScrollingParent这个接口,判断一下第一个view的位置,然后相应的去消费或者不消费滑动距离即可.
那么首先是onStartNestedScroll这个接口,我们只处理垂直方向的滑动,所以需要判断一下滑动的方向.
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes, int type) {
//只处理垂直方向的滑动
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
我们在垂直滑动的时候返回了true, 表示我们关系垂直方向的滑动,那么当有该方向的滑动发生时, recyclerview(已经实现了NestedScrollingChild接口) 会先询问我们是否要消费, 即回调onNestedPreScroll接口. 这里我们想一下消费的顺序: 初始状态下只能上滑, 头部有两部分, 一部分需要滑出屏幕外, 另外一部分要固定在屏幕顶端. 那么就是在第一个子view还没有滑动到最大可滑动距离(第一部分的高度)之前, 都由parent消费,其他情况则由recyclerview消费. 下滑的时候则正好相反,需要先让recycleview滑动,在recycleview还没有滑倒头时, 需要让recycleview先滑动, 否则则让parent来处理,让头部的控件滑动. 代码如下:
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
@ViewCompat.NestedScrollType int type) {
LinearLayoutManager linearLayoutManager =
(LinearLayoutManager) recyclerView.getLayoutManager();
//recycleview 是否滑到顶部
boolean isFirstItemVisible =
linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0;
//向上滑动,且头部没有滑倒最大高度,此时让header滑动
if (dy > 0 && header.getBottom() > mMaxScrollTop) {
int maxOffset = header.getBottom() - mMaxScrollTop;
int offset = Math.min(maxOffset, dy);
header.offsetTopAndBottom(offset * -1);
recyclerView.layout(recyclerView.getLeft(), recyclerView.getTop() - offset,
recyclerView.getRight(), recyclerView.getBottom());
consumed[1] = offset;
}
//向下滑动,且recycleview已经滑倒顶部, header没有滑倒原始位置前,让头部滑动
else if (dy < 0 && isFirstItemVisible && header.getBottom() < mOriginBottom) {
int offset = Math.min(mOriginBottom - header.getBottom(), dy * -1);
header.offsetTopAndBottom(offset);
recyclerView.layout(recyclerView.getLeft(), recyclerView.getTop() + offset,
recyclerView.getRight(), recyclerView.getBottom());
consumed[1] = -1 * offset;
}
}
完整的demo可以见: https://github.com/ljcmeng/NestedStickerHeaderView
最后是笔者在研究嵌套滑动时所遇到的一些坑,分享给大家.