一、CoordinatorLayout原理
CoordinatorLayout.Behavior接口中定义了嵌套滑动的相关方法具体如下。这些方法和NestedScrollingParent中定义的方法差不多。
public static abstract class Behavior<V extends View> {
// 这里还有事件分发的回调
// ......
// 这里还有事件分发的回调
// 下面都是嵌套滑动相关的方法
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
target, axes);
}
return false;
}
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
target, axes);
}
}
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onStopNestedScroll(coordinatorLayout, child, target);
}
}
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @NestedScrollType int type, @NonNull int[] consumed) {
// In the case that this nested scrolling v3 version is not implemented, we call the v2
// version in case the v2 version is. We Also consume all of the unconsumed scroll
// distances.
consumed[0] += dxUnconsumed;
consumed[1] += dyUnconsumed;
onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type);
}
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type) {
if (type == ViewCompat.TYPE_TOUCH) {
onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
}
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY,
boolean consumed) {
return false;
}
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY) {
return false;
}
}
CoordinatorLayout的子控件可以指定Behavior,这样事件分发和嵌套滑动就会交给Behavior处理。这里是什么实现的呢?
CoordinatorLayout实现了NestedScrollingParent,嵌套滑动相关方法以及事件分发相关方法都会遍历调用所有子控件的Behavior中定义的相关方法。
CoordinatorLayout的onStartNestedScroll方法如下,遍历子控件的CoordinatorLayout.Behavior,然后依次调用每个子控件的CoordinatorLayout.Behavior的onStartNestedScroll方法。如果有一个子控件的onStartNestedScroll方法会true,CoordinatorLayout的onStartNestedScroll方法就会返回true。嵌套滑动相关方法以及事件分发相关方法和onStartNestedScroll类似,都是遍历调用Behavior中对应的方法。
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,
NestedScrollingParent3 {
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
// 遍历子控件的CoordinatorLayout.Behavior,然后依次调用每个子控件的CoordinatorLayout.Behavior的onStartNestedScroll方法
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
}
二、AppBarLayout为什么可以滑动
AppBarLayout滑动分为两种情况:
- 滑动AppBarLayout引起的滑动
- 滑动底部的列表引起的滑动
1、滑动AppBarLayout引起的滑动
我们分别继承了MyCoordinatorLayout、MyAppbarLayout、MyAppbarBehavior以及MyAppbarScrollingViewBehavior,在onInterceptTouchEvent和onTouchEvent中打印日志。MyCoordinatorLayout重写如下,其他类类似。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "------------------------------------------------");
Log.e(TAG, "onInterceptTouchEvent>>start");
boolean result = super.onInterceptTouchEvent(ev);
Log.e(TAG, "onInterceptTouchEvent>>result:" + result);
Log.e(TAG, "------------------------------------------------");
return result;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e(TAG, "------------------------------------------------");
Log.e(TAG, "onTouchEvent>>start");
boolean result = super.onTouchEvent(ev);
Log.e(TAG, "onTouchEvent>>result:" + result);
Log.e(TAG, "------------------------------------------------");
return result;
}
当我们滑动AppBarLayout时打印日志如下:
com.demo.app E/MyCoordinatorLayout: ------------------------------------------------
com.demo.app E/MyCoordinatorLayout: onInterceptTouchEvent>>start
com.demo.app E/MyAppbarBehavior: onInterceptTouchEvent>>start
com.demo.app E/MyAppbarBehavior: onInterceptTouchEvent>>result:false
com.demo.app E/MyAppbarScrollingViewBehavior: onInterceptTouchEvent>>result:false
com.demo.app E/MyAppbarBehavior: onInterceptTouchEvent>>start
com.demo.app E/MyAppbarBehavior: onInterceptTouchEvent>>result:false
com.demo.app E/MyAppbarScrollingViewBehavior: onInterceptTouchEvent>>result:false
com.demo.app E/MyCoordinatorLayout: onInterceptTouchEvent>>result:false
com.demo.app E/MyCoordinatorLayout: ------------------------------------------------
com.demo.app E/MyAppbarLayout: ------------------------------------------------
com.demo.app E/MyAppbarLayout: onInterceptTouchEvent>>start
com.demo.app E/MyAppbarLayout: onInterceptTouchEvent>>result:false
com.demo.app E/MyAppbarLayout: ------------------------------------------------
com.demo.app E/MyAppbarLayout: ------------------------------------------------
com.demo.app E/MyAppbarLayout: onTouchEvent>>start
com.demo.app E/MyAppbarLayout: onTouchEvent>>result:false
com.demo.app E/MyAppbarLayout: ------------------------------------------------
com.demo.app E/MyCoordinatorLayout: ------------------------------------------------
com.demo.app E/MyCoordinatorLayout: onTouchEvent>>start
com.demo.app E/MyAppbarBehavior: onTouchEvent>>start
com.demo.app E/MyAppbarBehavior: onTouchEvent>>result:true
com.demo.app E/MyAppbarBehavior: onTouchEvent>>start
com.demo.app E/MyAppbarBehavior: onTouchEvent>>result:true
com.demo.app E/MyCoordinatorLayout: onTouchEvent>>result:true
com.demo.app E/MyCoordinatorLayout: ------------------------------------------------
com.demo.app E/MyCoordinatorLayout: ------------------------------------------------
com.demo.app E/MyCoordinatorLayout: onTouchEvent>>start
com.demo.app E/MyAppbarBehavior: onTouchEvent>>start
com.demo.app E/MyAppbarBehavior: onTouchEvent>>result:true
com.demo.app E/MyCoordinatorLayout: onTouchEvent>>result:true
com.demo.app E/MyCoordinatorLayout: ------------------------------------------------
通过上面的日志我们可以看出整个事件处理的流程。事件最先到CoordinatorLayout的onInterceptTouchEvent,CoordinatorLayout会调用Behavior的onInterceptTouchEvent,Behavior返回false不处理事件,即CoordinatorLayout的onInterceptTouchEvent返回false。
然后事件传递到AppbarLayout的onInterceptTouchEvent,AppbarLayout返回false不拦截事件。即所有控件都没有拦截事件。
然后事件传递到AppbarLayout的onTouchEvent,AppbarLayout不处理事件返回false。
然后事件向上传递到CoordinatorLayout的onTouchEvent,CoordinatorLayout会调用子控件Behavior的onTouchEvent处理事件,AppbarLayout的Behavior的onTouchEvent处理了事件返回true,因此CoordinatorLayout的onTouchEvent也会返回true。这代表事件最终由CoordinatorLayout处理,CoordinatorLayout又会在onTouchEvent中调用Behavior的onTouchEvent,即最终事件的处理是在Behavior的onTouchEvent中。
AppBarLayout.Behavior是继承HeaderBehavior,onTouchEvent实际在HeaderBehavior中实现的。代码如下:
public boolean onTouchEvent(
@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
boolean consumeUp = false;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(activePointerId);
if (activePointerIndex == -1) {
return false;
}
final int y = (int) ev.getY(activePointerIndex);
int dy = lastMotionY - y;
lastMotionY = y;
// We're being dragged so scroll the ABL
scroll(parent, child, dy, getMaxDragOffset(child), 0);
break;
...
}
}
在ACTION_MOVE时通过scroll方法实现滑动,scroll方法最终会执行ViewCompat.offsetTopAndBottom。
因此滑动AppBarLayout时AppBarLayout滑动是通过在Behavior的onTouchEvent中实现的。但是底部的列表怎么会滑动呢?
因为AppBarLayout的底部列表的父控件是CoordinatorLayout,AppBarLayout滑动时底部列表的Behavior的onDependentViewChanged会回调。底部列表的Behavior一般设置为app:layout_behavior=“@string/appbar_scrolling_view_behavior”,即AppBarLayout中的ScrollingViewBehavior。其关键代码如下:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof AppBarLayout;
}
@Override
public boolean onDependentViewChanged(
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
offsetChildAsNeeded(child, dependency);
updateLiftedStateIfNeeded(child, dependency);
return false;
}
layoutDependsOn中是依赖AppBarLayout,即当AppBarLayout滑动时,就会回调onDependentViewChanged,在offsetChildAsNeeded中实现列表的滑动。
2. 滑动底部的列表引起的滑动
滑动底部的引起整个布局滑动是通过嵌套滑动机制实现的,嵌套滑动机制详细介绍可以看如下文章:
NestedScrolling 嵌套滑动机制
滑动NestedScrollView时,根据嵌套滑动机制会先调用NestedScrollingChild(即NestedScrollView)的startNestedScroll方法。然后就会回调到NestedScrollingParent的onStartNestedScroll方法(这里的NestedScrollingParent是CoordinatorLayout)。根据上面的说明CoordinatorLayout.onStartNestedScroll中会遍历所有子控件的Behavior执行onStartNestedScroll。
首先会调用AppBarLayout的Behavior的onStartNestedScroll方法,代码如下。正常情况下,onStartNestedScroll()是会返回true,即CoordinatorLayout 的onStartNestedScroll也返回true,也就是告诉系统我要消费滑动。后面就会回调CoordinatorLayout 的onNestedPreScroll()方法,在CoordinatorLayout 的onNestedPreScroll()中会调用AppBarLayout的Behavior的onNestedPreScroll。AppBarLayout的Behavior的onNestedPreScroll方法如下:
// AppBarLayout的Behavior的onNestedPreScroll方法。
protected static class BaseBehavior<T extends AppBarLayout> extends HeaderBehavior<T> {
@Override
public boolean onStartNestedScroll(
@NonNull CoordinatorLayout parent,
@NonNull T child,
@NonNull View directTargetChild,
View target,
int nestedScrollAxes,
int type) {
// Return true if we're nested scrolling vertically, and we either have lift on scroll enabled
// or we can scroll the children.
final boolean started =
(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& (child.isLiftOnScroll() || canScrollChildren(parent, child, directTargetChild));
if (started && offsetAnimator != null) {
// Cancel any offset animation
offsetAnimator.cancel();
}
// A new nested scroll has started so clear out the previous ref
lastNestedScrollingChildRef = null;
// Track the last started type so we know if a fling is about to happen once scrolling ends
lastStartedType = type;
return started;
}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed, int type) {
if (dy != 0) {
int min, max;
if (dy < 0) {
// 向下滚动
min = -child.getTotalScrollRange(); // 该方法上面已经讲过了,获取到可滚动距离,然后取负数
max = min + child.getDownNestedPreScrollRange(); // 判断有没有向下滑动需要立即滑出的距离
} else {
// 向上滚动
min = -child.getUpNestedPreScrollRange(); // 内部就是getTotalScrollRange()
max = 0;
}
if (min != max) {
// 通过scroll方法进行滑动处理
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}
}
}
通过scroll实现AppBarLayout的滑动。根据上节介绍的,因为CoordinatorLayout的原因AppBarLayout滑动后就会回调列表的Behavior的onDependentViewChanged方法,然后实现列表的滑动,所以感觉AppBarLayout和列表整个界面在一起滑动。