写在前面

最近这一周空余的时间比较多,然后去重构了一个公司项目里的一个个人中心的页面,原来使用了ListView再加上addHead的方式,然后动态的去控制Head的高度去实现嵌套滑动的效果,因为我的模拟器没有跑起来,所以也没有去录下个GIF来,因为这篇主要说一下CoordinatorLayout的处理嵌套滑动的原理,没有效果图也影响不大。

开始吧

先写出来点大纲吧:

  • 为什么要有CoordinatorLayout
  • CoordinatorLayout是怎么实现嵌套滑动的事件分发的
  • AppBarLayout的一些东西

为什么要有CoordinatorLayout

先来回想一下Android系统的事件分发有什么不足的地方,就是一个view消费了事件,与此同时,其他的view的没有机会去接触到这个事件了。也就是在一个事件的某一时刻,有且只有一个View去相应这个事件。那么当我们要去做一些嵌套滑动的时候,就会有一些不方便。例如需要一个View跟随另一个View滑动,一般就是一个CallBack进行滑动状态的回调。CoordinatorLayout就可以比较优雅的处理这种事件。

CoordinatorLayout是怎么实现嵌套滑动的事件分发的

先说一下两个接口吧,NestedScrollingParentNestedScrollingChild,这两个接口看名字就比较容易理解他们的作用,就是一个给嵌套滑动的parent实现的,一个是给child实现,在现在的组件中,只有CoordinatorLayout实现了parent,所以下面说到parent就理解成CoordinatorLayout就OK啦,而child这个接口其实方法已经在View这个类里面帮我们实现好了,也就是说想要去成为一个可以嵌套滑动的child,仅仅去实现这个child接口就OK了,其他的View已经帮你写完了。下面还是以RecycleView为例子,它是实现了这个child的接口,还是回到情景里面说吧,说一下到底是怎么传递事件的。

  • ReclceView上down -> parent.onStartNestedScroll ->遍历有Behavior的直接子view ->child的Behavior.onStartNestedScroll
    这里说一下onStartNestedScroll这个方法是有boolean的返回值,true的意思是要去处理这个事件,同时还会去调用parent.onNestedScrollAccepted -> 遍历有Behavior的直接子view ->child的Behavior.onNestedScrollAccepted.这个方法里面可以做一些view配置的初始化。如果不需要当然可以不去重写。
  • RecycleView上move -> dispatchNestedPreScroll(也是child这个接口里面的)->parent.onNestedPreScroll -> 遍历有Behavior的直接子view ->child的Behavior.onNestedPreScroll,同时之后还有一个流程也是在move的时候触发的 ->dispatchNestedScroll(也是child这个接口里面的) ->parent.onNestedScroll -> 遍历有Behavior的直接子view ->child的Behavior.onNestedScroll.。这两个的主要区别的pre那个方法里面可以去处理消费了多少的滑动距离,比如手指滑动了12px,你可以选择把consumed[1]赋值为2,那么head就移动(消费)了2px,剩下的10px就给了下面的recycleview,而在onNestedScroll方法中还有机会去已经消费的和没有消费的距离再进行一次处理。
  • RecycleView上面up -> stopNestedScroll(也是child这个接口里面的) ->parent.onStopNestedScroll -> 遍历有Behavior的直接子view ->child的Behavior.onStopNestedScroll
  • 还有两个关于Fling的方法,就不说了,一个套路。

还有就是再说一下CoordinatorLayoutonInterceptTouchEventonTouchEvent,这里贴一下代码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
mBehaviorTouchView = child;
}
}

可以看出,每个直接的childview都会接受到拦截事件,即使你的手势不是在这个child上面触发的。

  • 还有个方法就是layoutDependsOn这个方法是在哪里调用的,这个是在onpredraw的时候就调用了,也就是没显示之前就已经开始准备各个view直接的依赖关系。当然内部实现也是在parent里面去便利child来实现的,也就只有parent可以获取到所有的child。

AppBarLayout的一些东西

我觉得AppBarLayout这个组件还是十分重要的,特别是里面的两个Behavior的实现,给了我们很好的参考的例子。具体的还是去看源码吧,太多了要说也是再开一篇。