一.前言
官方定义:
A Behavior implements one or more interactions that a user can take on a child view. These interactions may include drags, swipes, flings, or any other gestures.
中文:
一个Behavior实现了一个或多个用户可以采取的交互视图。这些相互作用可能包括拖动,滑动,快速滑动,或任何其他的手势。
二.类似观察者概念
其实Behavior就是一个应用于View的观察者模式,一个View跟随者另一个View的变化而变化,或者说一个View监听另一个View。
在Behavior中,被观察的View 也就是事件源被称为denpendcy,而观察View,则被称为child
Behavior内部的方法当在CoordinatorLayout上发生触摸事件的时候,CoordinatorLayout会处理触摸事件,并回调Behavior中的部分方法,我们可以通过处理这些回调方法来实现需求
三.Behavior中的几个重要方法
Behavior 是一个顶层抽象类,其他的一些具体行为的Behavior 都是继承自这个类。它提供了几个重要的方法:
layoutDependsOn
onDependentViewChanged
onStartNestedScroll
onNestedPreScroll
onNestedScroll
onStopNestedScroll
onNestedScrollAccepted
onNestedPreFling
onLayoutChild
/**
* 表示是否给应用了Behavior 的View 指定一个依赖的布局,通常,当依赖的View 布局发生变化时,不管被被依赖View 的顺序怎样,被依赖的View也会重新布局
* @param parent
* @param child 绑定behavior 的View
* @param dependency 依赖的view
* @return 如果child 是依赖的指定的View 返回true,否则返回false
*/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return super.layoutDependsOn(parent, child, dependency);
}
/**
* 当被依赖的View 状态(位置、大小等)发生变化时,这个方法被调用
* @param parent
* @param child
* @param dependency
* @return
*/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
return super.onDependentViewChanged(parent, child, dependency);
}
/**
* 当coordinatorLayout 的子View试图开始嵌套滑动的时候被调用。
* 当返回值为true的时候表明coordinatorLayout 充当nested scroll parent 处理这次滑动,需要注意的是只有当返回值为true的时候,Behavior 才能收到后面的一些nested scroll 事件回调(如:onNestedPreScroll、onNestedScroll等)这个方法有个重要的参数 nestedScrollAxes,表明处理的滑动的方向。
* @param coordinatorLayout 和Behavior 绑定的View的父CoordinatorLayout
* @param child 和Behavior 绑定的View
* @param directTargetChild
* @param target
* @param nestedScrollAxes
嵌套滑动 应用的滑动方向,看 {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
{@link ViewCompat#SCROLL_AXIS_VERTICAL}
* @return
*/
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
/**
* 嵌套滚动发生之前被调用
在nested scroll child 消费掉自己的滚动距离之前,嵌套滚动每次被nested scroll child
更新都会调用onNestedPreScroll。注意有个重要的参数consumed,可以修改这个数 组表示你消费了多少距离。假设用户滑动了100px,child 做了90px 的位移,你需要 把 consumed[1]的值改成90,
* 这样coordinatorLayout就能知道只处理剩下的10px的滚动。
* @param coordinatorLayout
* @param child
* @param target
* @param dx 用户水平方向的滚动距离
* @param dy 用户竖直方向的滚动距离
* @param consumed
*/
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
/**
* 进行嵌套滚动时被调用
* @param coordinatorLayout
* @param child
* @param target
* @param dxConsumed target 已经消费的x方向的距离
* @param dyConsumed target 已经消费的y方向的距离
* @param dxUnconsumed x 方向剩下的滚动距离
* @param dyUnconsumed y 方向剩下的滚动距离
*/
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}
/**
* 嵌套滚动结束时被调用,这是一个清除滚动状态等的好时机。
* @param coordinatorLayout
* @param child
* @param target
*/
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
super.onStopNestedScroll(coordinatorLayout, child, target);
}
/**
* onStartNestedScroll返回true才会触发这个方法,接受滚动处理后回调,可以在这个
方法里做一些准备工作,如一些状态的重置等。
* @param coordinatorLayout
* @param child
* @param directTargetChild
* @param target
* @param nestedScrollAxes
*/
@Override
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
/**
* 用户松开手指并且会发生惯性动作之前调用,参数提供了速度信息,可以根据这些 速度信息决定最终状态,比如滚动Header,是让Header处于展开状态还是折叠状 态。返回true 表示消费了fling.
* @param coordinatorLayout
* @param child
* @param target
* @param velocityX x方向的速度
* @param velocityY y方向的速度
* @return
*/
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
/*
* 可以重写这个方法对子View 进行重新布局
* @param coordinatorLayout
* @param child
* @param layoutDirection 布局的方向
*/
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
return super.onLayoutChild(parent, child, layoutDirection);
}
四.嵌套滚动原理和CoordinatorLayout
NestedScrolling提供了一套父View和子View滑动交互机制。要完成这样的交互,父View需要实现NestedScrollingParent接口,而子View需要实现NestedScrollingChild接口,系统提供的NestedScrollView控件就实现了这两个接口,这两个接口虽然有很多抽象方法,但均有NestedScrolling[Parent,Children]Helper辅助类来帮助处理大部分逻辑
NestedScroll的机制的简版是这样的,当子View在处理滑动事件之前,先告诉自己的父View是否需要先处理这次滑动事件,父View处理完之后,告诉子View它处理的多少滑动距离,剩下的还是交给子View自己来处理
CoordinatorLayout的事件传递
CoordinatorLayout并不会直接处理触摸事件,而是尽可能地先交由子View的Behavior来处理,它的onInterceptTouchEvent和onTouchEvent两个方法最终都是调用performIntercept方法,用来分发不同的事件类型分发给对应的子View的Behavior处理
//处理拦截或者自己的触摸事件
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = MotionEventCompat.getActionMasked(ev);
final List<View> topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList); //在5.0以上,按照z属性来排序,以下,则是按照添加顺序或者自定义的绘制顺序来排列
// Let topmost child views inspect first
final int childCount = topmostChildList.size();
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
// 如果有一个behavior对事件进行了拦截,就发送Cancel事件给后续的所有Behavior。假设之前还没有Intercept发生,那么所有的事件都平等地对所有含有behavior的view进行分发,现在intercept忽然出现,那么相应的我们就要对除了Intercept的view发出Cancel
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; //记录当前需要处理事件的View
}
}
// Don't keep going if we're not allowing interaction below this.
// Setting newBlock will make sure we cancel the rest of the behaviors.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child); //behaviors是否拦截事件
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
}
topmostChildList.clear();
return intercepted;
}