旧版本导入CoordinatorLayout依赖
implementation 'com.android.support:design:28.0.0'
升级Android X后的依赖
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.0.0'
NestedScrolling提供了一套父View和子View滑动交互机制;要完成这样的交互,父View需要实现NestedScrollingParent接口,而子View要实现NestedScrollingChild接口,系统提供了NestedScrollingView控件就实现了这两个接口;
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
NestedScrollingChild3, ScrollingView {}
NestedScrolling(嵌套滑动),就是子View和父View在滑动过程中,互相通信决定某个滑动是子View处理合适,还是父View处理合适;所以Parent和Child之间存在相互调用,NestedScrollView遵循下面的调用关系:
上图可以这么理解:
NestedScrollingParent和NestedScrollingChild都提供了对应非辅助类,NestedScrollingChildHelper和NestedScrollingParentHelper辅助类,辅助类会负责处理大部分逻辑,他们之间的调用关系如下:
NestedScrollingChild/2/3:此接口定义NestedScrolling各状态监听回调方法,需要子View去实现NestedScrollingChild接口,然后借助NestedScrollingChildHelper辅助类实现NestedScrolling各状态派发给父视图处理;
NestedScrollingChildHelper:实现了Child和Parent的交互逻辑,将NestedScrolling操作先交给Parent处理;
NestedScrollingParent/2/3:此接口定义接收子View的NestedScrolling各状态监听回调方法,需要父ViewGroup实现NestedScrollingParent接口,然后借助NestedScrollingParentHelper辅助类实现NestedScrolling各状态派处理;
NestedScrollingParentHelper:帮助Parent实现和Child交互的逻辑;滑动动作是Child的主动发起,Parent就受滑动回调并作出响应;
为什么是NestedScrollingChild接口?
NestedScrollingChild定义一套嵌套滑动事件标准方法(从开始startNestedScroll到结束stopNestedScroll),提供给子View实现,子View负责实现NestedScrollingChild的方法,根据事件的动作类型触发嵌套滑动事件标准方法,最终会调用实现了NestedScrollingParent父ViewGroup处理; 简单理解就是我告诉父ViewGroup我要滑动了,你可以先做滑动处理,然后我在处理;
实际上NestedScrollingChildHelper辅助类已经实现好了Child和Parent交互,原来View的处理逻辑滑动事件逻辑上大体不需要改变;需要做的就是,如果准备开始滑动了,需要告诉Parent,Child要准备进入滑动状态了,调用startNestedScroll();Child在滑动之前,先问一个你的Parent是否需要滑动,也就是调用dispatchNestedPreScroll;如果父类消耗了部分滑动事件,Child需要重新计算一下父类消耗后剩余给Child的滑动距离余量;然后,Child自己进行余下的滑动;最后,如果滑动距离还有剩余,Child就再问一下,Parent是否需要在继续滑动你剩下的距离,也就是调用dispatchNestedScroll(), 大概就是这么一回事,当然还会有和Scroll类型的Fling系列方法,但我们这里可以先忽略一下;
NestedScrollView的NestedScrollingChild接口实现都是交给辅助类NestedScrollingChildHelper来处理的,是否需要进行额外的一些操作要根据实际情况来定;
//NestedScrollChild
public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
//...
mParentHelper = new NestedScrollingParentHelper(this);
mChildHelper = new NestedScrollingChildHelper(this);
// ...because why else would you be using this widget?
setNestedScrollingEnabled(true);
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
//在初始化滚动的时候操作,一般在MotionEvent#ACTION_DOWN的时候调用
@Override
public boolean startNestedScroll(int axes) {
return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
}
@Override
public void stopNestedScroll() {
stopNestedScroll(ViewCompat.TYPE_TOUCH);
}
@Override
public boolean hasNestedScrollingParent() {
return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
}
//参数和dispatchNestedPreScroll方法的返回有关联
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
//在消费滚动事件之前调用,提供一个让ViewParent实现联合滚动的机会,因此ViewParent可以消费一部分或者全部的滑动事件,参数consumed会记录ViewParent所消费掉的事件
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
实现NestedScrollingChild接口挺简单的不是吗?但还需要我们决定什么时候进行调用,和调用哪些方法;
startNestedScroll和stopNestedScroll的调用
startNestedScroll配合stopNestedScroll使用,startNestedScroll会再接收到ACTION_DOWN的时候调用,stopNestedScroll会在接收到ACTION_UP|ACTION_CANCEL的时候调用,NestedScrollView中的伪代码是这样
onInterceptTouchEvent | onTouchEvent(MotionEvent ev){
case MotionEvent.ACTION_DOWN:
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_UP | ACTION_CANCEL:
stopNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
NestedScrollingChildHelper处理startNestedScroll方法,可以看出会调用Parent的onStartNestedScroll和onStartNestedScrollAccepted方法,只要Parent愿意优先处理这次的滑动事件,在结束的时候Parent还会收到onStopNestedScroll回调;
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null);
}
}
dispatchNestedPreScroll的调用
在消费滚动事件之前调用,提供一个让Parent实现联合滚动的机会,因此Parent可以消费一部分或者全部的滑动事件,注意参数consumed会记录了Parent所消费掉的事件
onTouchEvent(MotionEvent ev){
//..
case MotionEvent.ACTION_MOVE:
//..
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y; //计算偏移量
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
ViewCompat.TYPE_TOUCH)) {
deltaY -= mScrollConsumed[1]; //减去被消费掉的事件
//...
}
//...
break;
}
NestedScrollingChildHelper处理dispatchNestedPreScroll方法,会调用到上一步里记录的希望优先处理Scroll事件的Parent的onNestedPreScroll方法
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
//...
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
//...
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
//...
}
}
return false;
}
dispatchNestedScroll的调用
这个方法是在Child自己消费完成Scroll事件后调用的
onTouchEvent(MotionEvent ev){
//..
case MotionEvent.ACTION_MOVE:
//..
final int scrolledDeltaY = getScrollY() - oldY;// 计算这个Child View消耗掉的Scroll事件
final int unconsumedY = deltaY - scrolledDeltaY; //计算这个ChildView还没有消费掉Scroll事件
mScrollConsumed[1] = 0;
dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
ViewCompat.TYPE_TOUCH, mScrollConsumed);
NestedScrollingChildHelper处理dispatchNestedScroll方法,会调用上一步里记录的希望优先处理Scroll事件的Parent的onNestedScroll方法
private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
@NestedScrollType int type, @Nullable int[] consumed) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
//...
ViewParentCompat.onNestedScroll(parent, mView,
dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
//...
return true;
} else if (offsetInWindow != null) {
//...
}
}
return false;
}
为什么定义NestedScrollingParent接口?
NestedScrollingParent定义了父ViewGroup需要实现嵌套滑动的接口回调,父ViewGroup实现NestedScrollingParent接口,实现了NestedScrollingChild的子view会调用实现NestedScrollingParent的父ViewGroup回调方法,最终可以在父ViewGroup中确认是否处理滑动事件,然后子View处理剩余滑动事件;
同样也有一个NestedScrollingParentHelper辅助类来帮助Parent实现和Child交互的逻辑;滑动动作时Child主动发起,Parent接收滑动回调并作出响应;从上面的Child分析可知,滑动开始的调用startNestedScroll(),Parent收到onStartNestedScroll()回调,决定是否需要配合Child一起进行处理滑动,如果需要配合,还会回调onNestedScrollAccepted(),每次滑动前Child先询问Parent是否需要滑动,即dispatchNestedPreScroll(),这就回调到Parent的onNestedPreScroll(),Parent可以在这个回调中消费掉Child的Scroll事件,也就是优先于Child滑动;
Child滑动以后,会调用dispatchNestedScroll(),回调到Parent的onNestedScroll(),这里就是Child滑动后,剩下的给Parent处理,也就是后于Child滑动;
最后,滑动结束Child调用stopNestedScroll(),回调Parent的onStopNestedScroll()表示本次处理结束;
现在我们来看看NestedScrollingParent的实现细节,这里以CoordinatorLayout来分析而不再是NestedScrollView,因为它才是这篇文章的主角;
在这之前,首先简单介绍下Behavior这对象,你可以在XML中定义它就会在CoordinatorLayout中解析实例化到目标子View的LayoutParams或者获取到CoordinatorLayout子View的LayoutParams对象通过setter方法注入,如果你自定义的Behavior希望实现NestedScroll效果,那么你需要关注重写以下这些方法:
你会发现以上这些方法对应了NestedScrollingParent接口的方法,只是在参数上有所增加,且都会在CoordiantorLayout
实现 NestedScrollingParent
接口的每个方法中作出相应回调,下面来简单走读下这部分代码:
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {
//.....
//CoordiantorLayout的成员变量
private final NestedScrollingParentHelper mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
// 参数child:当前实现`NestedScrollingParent`的ViewParent包含触发嵌套滚动的直接子view对象
// 参数target:触发嵌套滚动的view (在这里如果不涉及多层嵌套的话,child和target)是相同的
// 参数nestedScrollAxes:就是嵌套滚动的滚动方向了.垂直或水平方法
//返回参数代表当前ViewParent是否可以触发嵌套滚动操作
//CoordiantorLayout的实现上是交由子View的Behavior来决定,并回调了各个acceptNestedScroll方法,告诉它们处理结果
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
//onStartNestedScroll返回true才会触发这个方法
//参数和onStartNestedScroll方法一样
//按照官方文档的指示,CoordiantorLayout有一个NestedScrollingParentHelper类型的成员变量,并把这个方法交由它处理
//同样,这里也是需要CoordiantorLayout遍历子View,对可以嵌套滚动的子View回调Behavior#onNestedScrollAccepted方法
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
mNestedScrollingDirectChild = child;
mNestedScrollingTarget = target;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes);
}
}
}
//嵌套滚动的结束,做一些资源回收操作等...
//为可以嵌套滚动的子View回调Behavior#onStopNestedScroll方法
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onStopNestedScroll(this, view, target);
}
lp.resetNestedScroll();
lp.resetChangedAfterNestedScroll();
}
mNestedScrollingDirectChild = null;
mNestedScrollingTarget = null;
}
//进行嵌套滚动
// 参数dxConsumed:表示target已经消费的x方向的距离
// 参数dyConsumed:表示target已经消费的y方向的距离
// 参数dxUnconsumed:表示x方向剩下的滑动距离
// 参数dyUnconsumed:表示y方向剩下的滑动距离
// 可以嵌套滚动的子View回调Behavior#onNestedScroll方法
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
final int childCount = getChildCount();
boolean accepted = false;
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,dxUnconsumed, dyUnconsumed);
accepted = true;
}
}
if (accepted) {
dispatchOnDependentViewChanged(true);
}
}
//发生嵌套滚动之前回调
// 参数dx:表示target本次滚动产生的x方向的滚动总距离
// 参数dy:表示target本次滚动产生的y方向的滚动总距离
// 参数consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0]): Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1]): Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
dispatchOnDependentViewChanged(true);
}
}
// @param velocityX 水平方向速度
// @param velocityY 垂直方向速度
// @param consumed 子View是否消费fling操作
// @return true if this parent consumed or otherwise reacted to the fling
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,consumed);
}
}
if (handled) {
dispatchOnDependentViewChanged(true);
}
return handled;
}
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
}
}
return handled;
}
//支持嵌套滚动的方向
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
}
你会发现CoordinatorLayout收到来自NestedScrollingChild的各种回调后,都是交由需要响应的Behavior来处理,所以这样可以得出一个结论,CoordinatorLayout是Behavior的一个代理类,所以Behavior实际上也是一个NestedScrollingParent,另外结合NestedScrollingChild实现的部分来看,你很容易就能搞懂这些方法参数的实际含义;
CoordinatorLayout,Behavior和NestedScrollingParent三者关系:
NestedScroll的机制简版是这样的,当子View在处理滑动事件之前,先告诉自己的父View是否需要先处理这次滑动事件,父View处理完之后,告诉子View它处理了多少滑动距离,剩下的还是交给子View自己来处理;
你也可以实现这样的一套机制,父View拦截所有事件,然后分发给需要的子View来处理,然后剩余的自己来处理;但是这样就做会使得逻辑处理更复杂,因为事件的传递本来就由外向内传递到子View,处理机制是由内向外的,由子View先来处理事件本来就是遵守默认规则的;
上面再分析NestedScrollParent接口的时候已经简单提到了CoordinatorLayout这个控件,至于这个控件是用来做什么的?CoordinatorLayout内部有个Behavior对象,这个Behavior对象可以通过外部setter或者xml中指定的方式注入到CoordinatorLayout的某个子View的LayoutParams,Behavior对象定义了特定类型的视图交互逻辑,譬如FloatingActionButton的Behavior实现类,只要FloatingActionButton是CoordinatorLayout的子View,且设置的该Behavior(默认已经设置了),那么,这个FAB就会在Snackbar出现的时候上浮,而不至于被遮挡,而这种通过定义Behavior的方式可以控制View的某一类的行为,通常会比自定义View的方式更解耦更轻便,由此可知,Behavior是CoordinatorLayout的精髓所在;
简单来看看Behavior是如何从xml中解析的,通过检测xxx:behavior属性,通过全限定名或者相对路径的形式指定路径,最后通过反射来新建实例,默认的构造器是Behavior(Context context, AttributeSet attrs),如果需要配置额外的参数,可以在外部构造好Behavior并通过setter的方式注入到LayoutParams或者获取到解析好的Behavior进行额外的参数设定;
LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_Layout);
//...
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
a.recycle();
if (mBehavior != null) {
// If we have a Behavior, dispatch that it has been attached
mBehavior.onAttachedToLayoutParams(this);
}
}
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
final String fullName;
if (name.startsWith(".")) {
// Relative to the app package. Prepend the app package name.
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// Fully qualified package name.
fullName = name;
} else {
// Assume stock behavior in this package (if we have one)
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
try {
//...
if (c == null) {
final Class<Behavior> clazz =
(Class<Behavior>) Class.forName(fullName, false, context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
//...
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
View之间的依赖关系
CoordinatorLayout的子View可以扮演着两种不同角色,一种是被依赖的,而另一种则是主动寻找依赖的View,被以来的View并不会感知到自己被依赖,被依赖的View也有可能是寻找依赖的View;
这种依赖关系的建立由CoordinatorLayout#LayoutParams来指定,假设此时两个View:A和B,那么有两种情况导致依赖关系
LayoutParams中关于依赖的判断的依据的代码如下
LayoutParams
boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency == mAnchorDirectChild
|| shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
|| (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}
依赖判断通过两个条件判断,一个生效即可,最容易理解的根据Behavior#layoutDependsOn方法指定,例如View依赖AppBarLayout
AppBarLayout#BaseBehavior
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
另外一个可以看到是通过mAnchorDirectChild来判断,首先要知道AnchorView的ID是通过setter或者xml的anchor属性形式指定,但是为了不需要每次都根据ID通过findViewById去解析出AnchorView,所以会使用mAnchorView变量缓存好,需要注意的是这个AnchorView不可以是CoordinatorLayout,另外也不可以是当前View的一个子View,变量mAnchorDirectChild记录的就是AnchorView的所属的ViewGroup或自身(当它直接ViewParent是CoordinatorLayout的时候),关于AnchorView的作用,也可以在FAB配合AppBarLayout使用的时候,AppBarLayout会做为FAB的AnchorView,就可以在AppBarLayout打开或者收缩状态的时候显示或者隐藏FAB,自己这方面的实践比较少,在这也可以先忽略并不影响后续分析,大家感兴趣的可以通过看相关代码一探究竟;
根据这种依赖关系,CoordinatorLayout中维护了一个mDependencySortedChildren列表,里面含有所有子View,按照依赖关系排序,被依赖着排在前面,会在每次测量前重新排序,确保处理的顺序是**被依赖的View会先被measure和layout**;
@Override
@SuppressWarnings("unchecked")
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
//...
}
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
mChildDag.addNode(view);
//按照View之间的依赖关系,存储View
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
// Make sure that the other node is added
mChildDag.addNode(other);
}
// Now add the dependency to the graph
mChildDag.addEdge(other, view);
}
}
}
// mChildDag.getSortedList()会返回一个按照依赖关系排序后的View集合
// 被依赖的View排在前边,没有被依赖的在后边
mDependencySortedChildren.addAll(mChildDag.getSortedList());
// We also need to reverse the result since we want the start of the list to contain
// Views which have no dependencies, then dependent views after that
Collections.reverse(mDependencySortedChildren);
}
很明显prepareChildren()就是完成CoordinatorLayout中子View按照依赖关系的排序,被依赖的View排在前面,并将结果保存在mDependencySortedChildren中,在每次测量前都会重新排序;
mDependencySortedChildren依赖DAG排序,有向无环图(Directed Acyclic Graph, DAG)是有向图的一种,字面意思的理解就是图中没有环。常常被用来表示事件之间的驱动依赖关系,管理任务之间的调度。
这种依赖关系确定后又有什么作用呢?当然是在主动寻找依赖的View,在其依赖的View发生变化的时候,自己能够知道啦,也就是如果CoordinatorLayout内的A依赖B,在B的大小位置等发生状态变化的时候,A可以监听到,并作出响应,CoordinatorLayout又是怎么实现的呢?
CoordinatorLayout本身注册了两种监听器,ViewTreeObserver.OnPreDrawListener和onHierarchyChangeListener,一种是在绘制的之前进行回调,一种是在子View的层级结构发生变化的时候回调,有这两种监听就可以在接收到被依赖的View变化了;
监听提供依赖的视图的位置变化
OnPreDrawListener在CoordinatorLayout绘制之前回调,因为在layout之后,所以可以很容易判断到某个View的位置是否发生的变化
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
onChildViewsChanged方法,会遍历根据依赖关系排序好的子View集合,找到位置改变了的View,并回调依赖这个View的Behavior的onDependentViewChanged方法
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
// Do not try to update GONE child views in pre draw updates.
continue;
}
// Check child views before for anchor
//...
// 获取child当前的绘画区域
getChildRect(child, true, drawRect);
//...
if (type != EVENT_VIEW_REMOVED) {
// Did it change? if not continue
getLastChildRect(child, lastDrawRect);
//比较前后两次位置变化,位置没有发生改变就进入下次循环得了
if (lastDrawRect.equals(drawRect)) {
continue;
}
//记录上一次View的绘画位置Rect
recordLastChildRect(child, drawRect);
}
// 如果发生改变了,往后面位置中找到依赖当前View的Behavior来进行回调
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
//判断后面位置指定的Behavior的View是否依赖child视图
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
//...
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// Otherwise we dispatch onDependentViewChanged()
//回调对应指定Behavior的View提示View依赖的视图child发生位置变化了
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
//...
}
}
}
//...
}
监听提供依赖的View的添加和移除
HierarchyChangeListener在View的添加和移除都会回调
private class HierarchyChangeListener implements OnHierarchyChangeListener {
//...
@Override
public void onChildViewRemoved(View parent, View child) {
onChildViewsChanged(EVENT_VIEW_REMOVED);
//...
}
}
根据情况回调Behavior#onDependentViewRemoved
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
// Do not try to update GONE child views in pre draw updates.
continue;
}
// Check child views before for anchor
//...
// 获取child当前的绘画区域
getChildRect(child, true, drawRect);
//...
if (type != EVENT_VIEW_REMOVED) {
// Did it change? if not continue
getLastChildRect(child, lastDrawRect);
//比较前后两次位置变化,位置没有发生改变就进入下次循环得了
if (lastDrawRect.equals(drawRect)) {
continue;
}
//记录上一次View的绘画位置Rect
recordLastChildRect(child, drawRect);
}
// 如果发生改变了,往后面位置中找到依赖当前View的Behavior来进行回调
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
//判断后面位置指定的Behavior的View是否依赖child视图
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
//...
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
//回调对应指定Behavior的View提示View依赖的视图child被移除了
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// Otherwise we dispatch onDependentViewChanged()
//回调对应指定Behavior的View提示View依赖的视图child发生位置变化了
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
//...
}
}
}
//...
}
我们可以按照两种目的来实现自己的Behavior,当然也可以两种都实现啦;
第一种情况需要重写layoutDependsOn和OndependentViewChanged方法;
第二种情况需要重写onStartNestedScroll和onNestedPreScroll系列方法(上面已经提到了);
对于第一种情况,我们之前分析依赖的监听的时候响应回调细节已经说完了,Behavior只需要在onDependentViewChanged做相应的处理就好;
对于第二种情况,我们在NestedScroll的那节也已经把相关回调细节说了;
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 = ev.getActionMasked();
final List<View> topmostChildList = mTempList1;
//在5.0以上,按照z属性来排序,以下,则是按照添加顺序或者自定义的绘制顺序来排列
getTopSortedChildren(topmostChildList);
// 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);
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;
}
以上,基本可以理清CoordinatorLayout的机制,一个View如何监听到依赖View的变化,和CoordinatorLayout中的NestedScrollingChild实现NestedScroll的机制,触摸事件又是如何被Behavior拦截和处理,另外还有测量和布局我在这里并没有提及,但基本就是按照依赖关系排序,遍历子View,询问他们的Behavior是否需要处理,大家可以翻翻源码,这样可以有更深刻的体会,有了这些知识,我们基本就可以根据需要来自定义自己的Behavior了,下面也带大家来实践下我是如何用自定义的Behavior实现相关功能的;
简单翻一下源码即可;
CoordinatorLayout容器在测量子视图时,首先获取子视图的Behavior,确认指定Behavior子视图是否自己代理测量视图,返回true,则CoordinatorLayout容器不负责指定Behavior子视图测量工作;
CoordinatorLayout容器在布局子视图时,和指定Behavior测量子视图的逻辑类似,指定Behavior子视图代理布局子视图;
Android CoordinatorLayout之源码解析 - 简书
自定义Behavior的艺术探索-仿UC浏览器主页 - 简书