CoordainatorLayout作为控制内部一个或多个的子控件协同交互的容器,通过设置Behavior去控制多个控件的协同交互效果,测量尺寸、布局位置及触摸响应。
Behavior的具体用法如下图所示:
控件之间的相互依赖
首先我们需要在回到Behavior在哪里进行初始化,阅读CoordinatorLayout的源码我们知道,Behavior是在CoordinatorLayout的内部类LayoutParams的构造函数进行初始化。
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);//1
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));//2
}
a.recycle();
if (mBehavior != null) {
// If we have a Behavior, dispatch that it has been attached
mBehavior.onAttachedToLayoutParams(this);
}
}
1处首先判定有否在布局中有layout_behavior这个标签。例如:
= 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 {
Map> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor c = constructors.get(fullName);
if (c == null) {
final Class clazz =
(Class) Class.forName(fullName, false, context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
先抛出一个自己在实际操作的过程中遇到的问题,就是自定义Behavior的时候,没有添加对应的构造函数而发生崩溃。自己定义的代码如下:
public class TranslationBehavior extends FloatingActionButton.Behavior {
//必须添加这个构造函数,不然会崩了
/**
* parseBehavior():c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
* static final Class>[] CONSTRUCTOR_PARAMS = new Class>[] {
* Context.class,
* AttributeSet.class
* };
* 需要解析这两个参数的构造函数
* @param context
* @param attrs
*/
public TranslationBehavior(Context context, AttributeSet attrs){
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
private boolean isOut = false;
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
if (dyConsumed > 0){
if (!isOut){
int mY = ((CoordinatorLayout.LayoutParams)child.getLayoutParams()).bottomMargin + child.getMeasuredHeight();
child.animate().translationY(mY).setDuration(1000).start();
isOut = true;
}
}else {
if (isOut){
child.animate().translationY(0).setDuration(1000).start();
isOut = false;
}
}
}
}
先分析原来的parseBehavior()再来解释为什么会崩溃。
parseBehavior()主要做了以下两件事:
1、如果在设置app:layout_behavior=".xx.xx.xx.xxxxBehavior”标签的时候,后面的部分只写了.xxxxBehavior,系统的则将包名与这个名字拼接,如果是像上面那样全拼,则直接就是返回这个全名。
2、根据上面得到的名字从一个map里面去查找对应的Behavior,如果没有找到,采用反射的方式创建Behavior实例。并保存起来,但是在采用反射的过程中,用的是含有两个参数的构造方法进行创建。
所以在我们自定义的Behavior中,如果我们不构建一个两个参数的构造函数,在进行解析的时候就会报错。
下面继续来分析控件之间的相互依赖:
由于View的生命周期的开始是在onAttachedToWindow方法中,在CoordinatorLayout类中找到onAttachedToWindow方法
发现它调用getViewTreeObserver,获得ViewTreeObserver,然后调用了addOnPreDrawListener,ViewTreeObserver 注册一个观察者来监听视图树,当视图树的布局、视图树的焦点、视图树将要绘制、视图树滚动等发生改变时,ViewTreeObserver都会收到通知,ViewTreeObserver不能被实例化,可以调用View.getViewTreeObserver()来获得
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors(false);
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
mIsAttachedToWindow = true;
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
在OnPreDrawListener中调用了onChildViewChanged()。
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();
......
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();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {//1
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
checkLp.resetChangedAfterNestedScroll();
continue;
}
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()
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
}
}
}
}
首先看DispatchChangeEvent,这个有三个值,分别为:EVENT_PRE_DRAW, EVENT_NESTED_SCROLL, EVENT_VIEW_REMOVED,分别表示绘制前,需要滚动,和移除。
在1处调用layoutDependsOn()先判断看一下这个child是不是被依赖的。使用了一个名为mDependencySortedChildren的集合,通过遍历该集合,我们可以获取集合中控件的LayoutParam,得到LayoutParam后,我们可以继续获取相应的Behavior。并调用其layoutDependsOn方法找到所依赖的控件,如果找到了当前控件所依赖的另一控件,那么就调用Behavior中的onDependentViewChanged方法。
所以我们在具体操作的时候我们可以如下操作:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
boolean dependOn = isDependOn(dependency);
Log.i(TAG, "layoutDependsOn: dependOn =" + dependOn);
return dependOn;
}
private boolean isDependOn(View dependency) {
return dependency != null && dependency.getId() == mDependsLayoutId;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
offsetChildAsNeeded(parent, child, dependency);
return false;
}
private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
/* int translationY = (int) (denpendencyTranslationY / (getHeaderOffsetRange() * 1.0f)
* getScrollRange(dependency));*/
float denpendencyTranslationY = dependency.getTranslationY();
Log.d(TAG, "offsetChildAsNeeded: denpendencyTranslationY=" + denpendencyTranslationY
+ " denpendencyTranslationY=" + denpendencyTranslationY);
// child.setTranslationY(translationY);
// is a negative number
int maxTranslationY = -(dependency.getHeight() - getFinalY());
if (denpendencyTranslationY < maxTranslationY) {
denpendencyTranslationY = maxTranslationY;
}
child.setTranslationY((int) (denpendencyTranslationY));
}
CoordinatorLayout内部嵌套滑动原理
先看一张图,如下:
场景:一个RecyclerView和一个FloatingActionButton,当RecyclerView向上滑的时候,FloatingActionButton则隐藏,否则则显示。
我们都知道,滑动事件都是围绕onInterceptTouchEvent与onTouchEvent方法展开的。下面来看
CoordinatorLayout的onInterceptTouchEvent和onTouchEvent事件。
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors(true);
}
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(true);
}
return intercepted;
}
从上面可以看到,onInterceptTouchEvent的返回值是根据performIntercept的返回值来定,
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
final List topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList);//1
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();
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
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;
}
}
}
topmostChildList.clear();
return intercepted;
}
在1处,我们对view根据Z轴进行排序,然后进行查询,由于intercepted和newBlock一开始都为false,所以第一个if不会进去,进入第二个if,而intercepted又是根据Behavior的onInterceptTouchEvent()来决定,而Behavior的onInterceptTouchEvent()默认是返回false,相当于不拦截。则会调用子类的onInterceptTouchEvent()。
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(e);
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mIgnoreMotionEventTillDown) {
mIgnoreMotionEventTillDown = false;
}
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
if (mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
stopNestedScroll(TYPE_NON_TOUCH);
}
// Clear the nested offsets
mNestedOffsets[0] = mNestedOffsets[1] = 0;
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);//1
break;
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
if (mScrollState != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = y;
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
}
break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.clear();
stopNestedScroll(TYPE_TOUCH);
}
break;
case MotionEvent.ACTION_CANCEL: {
cancelScroll();
}
}
return mScrollState == SCROLL_STATE_DRAGGING;
}
在1处,当手指按下的时候,会调用startNestedScroll(),
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
getScrollingChildHelper()返回的是NestedScrollingChildHelper的实例。
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;
}
会调用父类的onStartNestedScroll().并且onStartNestedScroll()返回true时,会调用父类的的onNestedScrollAccepted()。
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
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;
}
在父类中,优惠找对应的Behavior,然后调用Behavior的onStartNestedScroll()。结合最开始的那张图,发现对应的流程为,先由子类开始发起,然后询问父类,父类最后有调用Behavior中对应的方法。
在回到onInterceptTouchEvent()的ACTION_MOVE。在ACTION_MOVE中,将状态设置为SCROLL_STATE_DRAGGING。看返回值则为true,表示子类拦截,根据事件分发流程,自己拦截,将会交给自己的TouchEvent().
public boolean onTouchEvent(MotionEvent e) {
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
......
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
if (dispatchNestedPreScroll(//1
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
}
}
break;
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetScroll();//2
}
break;
case MotionEvent.ACTION_CANCEL: {
cancelScroll();//3
}
break;
}
return true;
}
在1处,直接调用dispatchNestedPreScroll(),该方法表示在子控件滑动前,将事件分发给父控件,由父控件判断消耗多少。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
type);
}
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (dx != 0 || dy != 0) {
......
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
.....
return consumed[0] != 0 || consumed[1] != 0;
}
}
return false;
}
进而会调用父类的onNestedPreScroll()。
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
}
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
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 Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
.......
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mBehaviorConsumed, type);
.......
accepted = true;
}
}
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
最终调用的是Behavior的onNestedPreScroll。
当为UP和ACTION_CANCEL状态时,会调用stopNestedScroll(),其后面的分析跟前面的一样。
所以总结如下面的图所示:
Behavior的布局
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
先从mDependencySortedChildren拿取View,先判断它是否可见,不可见则跳过。
然后找对应的Behavior,如果Behavior有,则调用Behavior的onLayoutChild(),判断Behavior是否有自己的布局,如果有则按Behavior的布局走,没有则调用自己的onLayoutChild()。onLayoutChild中调用了layoutChild()。layoutChild里面调用onLayout().
Behavior的测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//省略部分代码….
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childWidthMeasureSpec = widthMeasureSpec;
int childHeightMeasureSpec = heightMeasureSpec;
final Behavior b = lp.getBehavior();
//调用Behavior的测量方法。
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
lp.leftMargin + lp.rightMargin);
heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin);
childState = View.combineMeasuredStates(childState, child.getMeasuredState());
}
final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
childState & View.MEASURED_STATE_MASK);
final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
childState << View.MEASURED_HEIGHT_STATE_SHIFT);
setMeasuredDimension(width, height);
}
观察上述代码,我们发现该方法与CoordinatorLayout的布局逻辑非常相似,也是对子控件进行遍历,并调那个用子控件的Behavior的onMeasureChild方法,判断是否自主测量,如果为true,那么则以子控件的测量为准。当子控件测量完毕后。会通过widthUsed 和 heightUsed 这两个变量来保存CoordinatorLayout中子控件最大的尺寸。这两个变量的值,最终将会影响CoordinatorLayout的宽高。
在实际开发中遇到一个问题,布局为在CoordinatorLayout中包裹着一个头部和ViewPager,向上滑动将将头部隐藏,发现ViewPAger下面将会留下一段空白。
发生这个情况的原因是,根据view的测量规则,viewpager的高度为屏幕的高度-头部的高度。当头部滚出屏幕后,就会出现一段空白。
解决方法:
重写onMeasure(),然后做如下操作。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams();
layoutParams.height = getMeasuredHeight() - mNavView.getMeasuredHeight();
mViewPager.setLayoutParams(layoutParams);
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}