CoordinatorLayout详解及自定义Behavior

总览

  CoordinatorLayout-协调布局,用于实现自定义的嵌套滑动效果。RecyclerView(NestedScrollingChild2)会把滑动事件传递给CoordinatorLayout(NestedScrollingParent2),CoordinatorLayout最后会把滑动事件传递给其直接子View中设置了layout_behavior的控件,用户只需通过自定义CoordinatorLayout.Behavior然后在各个回调函数中去实现自己的逻辑即可。下边列出嵌套滑动中常用的回调函数:
NestedScrollingChild2

  • boolean startNestedScroll(int axes, int type)
    开始嵌套滑动时回调函数,只有返回了true才会继续分发后续嵌套滑动事件
  • void stopNestedScroll( int type)
    结束嵌套滑动时回调
  • boolean hasNestedScrollingParent(int type)
    是否有支持嵌套滑动的Parent
  • boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type)
    滑动事件已经产生,且在RecyclerView消耗滑动事件之前进行回调
  • boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type)
    滑动事件已经产生,且在RecyclerView消耗事件之后进行回调

NestedScrollingParent2

  • boolean onStartNestedScroll(View child, View target, int axes,int type)
    开始嵌套滑动时回调函数,只有返回了true才能继续接收后续滑动事件
  • void onNestedScrollAccepted(View child, View target, int axes,int type)
    onStartNestedScroll返回true后,该函数被调用,表示可以嵌套滑动
  • void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type)
    滑动事件已经产生,且在RecyclerView消耗滑动事件之前进行回调
  • void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type)
    滑动事件已经产生,且在RecyclerView消耗事件之后进行回调
  • void onStopNestedScroll(View target, int type)
    嵌套滑动结束时回调

RecyclerView.Behavior

  • public boolean layoutDependsOn(CoordinatorLayout parent, V child,View dependency)
    确定内部view之间的依赖关系,被依赖的View位置信息发生变化时会通知所以依赖于它的View
  • public boolean onDependentViewChanged(CoordinatorLayout parent, V child,View dependency)
    被依赖的View位置信息发生变化, 通知依赖它的View时回调的函数
  • public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int axes, int type)
    开始嵌套滑动时回调函数,只有返回了true才能继续接收后续滑动事件
  • public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type)
    嵌套滑动结束时回调
  • public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed, int type)
    滑动事件已经产生,且在RecyclerView消耗滑动事件之前进行回调
  • onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type)
    滑动事件已经产生,且在RecyclerView消耗事件之后进行回调

嵌套事件的分发

首先RecyclerView接收到ACTION_DOWN事件,然后开始回调startNestedScroll:

public boolean onTouchEvent(MotionEvent e) {
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
        }
}

嵌套滑动相关的回调都是通过一个NestedScrollingChildHelper来进行代理的:

public boolean startNestedScroll(int axes, int type) {
    return getScrollingChildHelper().startNestedScroll(axes, type);
}

那么具体的实现就来分析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) {
		    //兼容性处理,后续分析都看作直接调用CoordinatorLayout相关函数
			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;
}

首先会检查是否有支持嵌套滑动的Parent,它在NestedScrollingChildHelper内部保存:

public class NestedScrollingChildHelper {
    private ViewParent mNestedScrollingParentTouch;
}

  第一次调用一定返回false。然后回到上边的while循环,这里主要就是去找到支持嵌套滑动的Parent,并赋值给mNestedScrollingParentTouch,这里有几个注意点:

  1. RecyclerView不一定要是CoordinatorLayout的直接子类,只要能在包含RecyclerView的View中找到在onStartNestedScroll(View child, View target, int axes,int type)中返回true的,那么都可以支持嵌套滑动。
  2. onStartNestedScroll中child和target。target始终代表触发滑动事件的那个View,在这里是RecyclerView。而对于child,首先它一定是CoordinatorLayout的直接子类,其次它的子View当中一定包含那个触发滑动事件的RecyclerView。一般情况下,RecyclerView都会是CoordinatorLayout的直接子类,那么child和target就是同一个对象即RecyclerView。

在这里会回调CoordinatorLayout的onStartNestedScroll(View child, View target, int axes,int type)

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) {
			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;
}

  这里逻辑也很清晰,遍历子View,从LayoutParams上找到layout_behavior,如果有那么就会把事件传递给自定义的 Behavior.onStartNestedScroll,这里也有几个注意点:

  1. onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int axes, int type)中的参数问题。
    • child:CoordinatorLayout中当前index对应的View
    • directTargetChild:包含触发嵌套滑动RecyclerView的顶层View,它必须是CoordinatorLayout的直接子view
    • target:触发嵌套滑动的RecyclerView
  2. 只要CoordinatorLayout任意一个子View消耗掉了此次DOWN事件,那么CoordinatorLayout就会接收后续的嵌套滑动事件

   Behavior.onStartNestedScroll默认返回false,也就是我们在自定义Behavior时,需要在该方法中决定是否要支持嵌套滑动。一般是如下实现方式:

 public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, 
 View target, int axes, int type) {
	if (target instanceof RecyclerView) {
	    return true;
	} 
	return false;
}

   整个DOWN事件处理完后,其实就是做了一件事,确定是否有Parent支持嵌套滑动,并记录它,以此决定是否要分发后续的嵌套滑动事件,那么接下来看看滑动事件是如何分发的:

onTouchEvent(MotionEvent e) {
   switch (action) {
   	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;

   		mReusableIntPair[0] = 0;
   		mReusableIntPair[1] = 0;
   		if (dispatchNestedPreScroll(dx, dy, mReusableIntPair, mScrollOffset, TYPE_TOUCH)) {
   			dx -= mReusableIntPair[0];
   			dy -= mReusableIntPair[1];
   			vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
   			mNestedOffsets[0] += mScrollOffset[0];
   			mNestedOffsets[1] += mScrollOffset[1];
   		}
   	}
   }
}

   这里调用了dispatchNestedPreScroll(dx, dy, mReusableIntPair, mScrollOffset, TYPE_TOUCH),其内部是调用NestedScrollingChildHelper.dispatchNestedPreScroll(),直接看源码:

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;
               if (offsetInWindow != null) {
                   mView.getLocationInWindow(offsetInWindow);
                   startX = offsetInWindow[0];
                   startY = offsetInWindow[1];
               }

               if (consumed == null) {
                   consumed = getTempNestedScrollConsumed();
               }
               consumed[0] = 0;
               consumed[1] = 0;
               ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

               if (offsetInWindow != null) {
                   mView.getLocationInWindow(offsetInWindow);
                   offsetInWindow[0] -= startX;
                   offsetInWindow[1] -= startY;
               }
               return consumed[0] != 0 || consumed[1] != 0;
           } else if (offsetInWindow != null) {
               offsetInWindow[0] = 0;
               offsetInWindow[1] = 0;
           }
       }
       return false;
   }

   首先会检查是否设置了可以处理嵌套滑动的Parent(设置Parent的地方就在之前DOWN事件时调用的NestedScrollingChildHelper.startNestedScroll),最终还是回调了CoordinatorLayout.onNestedPreScroll()

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);
   	if (view.getVisibility() == GONE) {
   		continue;
   	}

   	final LayoutParams lp = (LayoutParams) view.getLayoutParams();
   	if (!lp.isNestedScrollAccepted(type)) {
   		continue;
   	}

   	final Behavior viewBehavior = lp.getBehavior();
   	if (viewBehavior != null) {
   		mTempIntPair[0] = mTempIntPair[1] = 0;
   		viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
          //记录子View中消耗最多的滑动距离
   		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;
   	}
   }
   //记录消耗的位移量,也就是用户先于Recycler消耗掉了多少位移量
   consumed[0] = xConsumed;
   consumed[1] = yConsumed;

   if (accepted) {
   	onChildViewsChanged(EVENT_NESTED_SCROLL);
   }
}

   遍历子View,对非GONE状态的View,获取它的Behavior,调用Behavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type),默认是空实现,自定义Behavior时,重写该方法去实现自己的滑动逻辑,这里消耗掉的位移量,需要手动设置consumed数组去记录,否则RecyclerView还是会消耗所有的滑动位移。这里有一个注意点是:

  • CoordinatorLayout记录的滑动消耗量是其所有子View当中消耗量最多的那两个。
  • 最后回调了一个onChildViewsChanged(EVENT_NESTED_SCROLL)函数,内部去处理一些依赖关系的事件传递。为了不影响主流程的分析,这里先跳过放到后边单独分析。

   CoordinatorLayout消耗完滑动事件后,继续回到RecyclerView的MOVE事件中,更新滑动量后,调用自身的scrollByInternal(int x, int y, MotionEvent ev)函数来处理剩余的滑动量:

//保留核心代码
boolean scrollByInternal(int x, int y, MotionEvent ev) {
	int unconsumedX = 0;
	int unconsumedY = 0;
	int consumedX = 0;
	int consumedY = 0;

	if (mAdapter != null) {
		mReusableIntPair[0] = 0;
		mReusableIntPair[1] = 0;
		scrollStep(x, y, mReusableIntPair);
		consumedX = mReusableIntPair[0];
		consumedY = mReusableIntPair[1];
		unconsumedX = x - consumedX;
		unconsumedY = y - consumedY;
	}

	mReusableIntPair[0] = 0;
	mReusableIntPair[1] = 0;
	dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
			TYPE_TOUCH, mReusableIntPair);
	unconsumedX -= mReusableIntPair[0];
	unconsumedY -= mReusableIntPair[1];

	if (consumedX != 0 || consumedY != 0) {
		dispatchOnScrolled(consumedX, consumedY);
	}

	return consumedX != 0 || consumedY != 0;
}

   首先调用scrollStep(x, y, mReusableIntPair)计算内部消耗的滑动偏移量,存放于mReusableIntPair数组中,而scrollStep(x, y, mReusableIntPair)内部是调用了LayoutManager去计算滑动消耗量的,这里就不深究内部计算逻辑了。计算完内部消耗的滑动量之后,记录一下已经消耗的和暂未消耗的滑动量,再调用dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset, TYPE_TOUCH, mReusableIntPair)去分发滑动事件,最终还是回调的NestedScrollingChildHelper.dispatchNestedScrollInternal()

//保留核心代码
dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, 
    int[] offsetInWindow,int type, int[] consumed) {
	if (isNestedScrollingEnabled()) {
		final ViewParent parent = getNestedScrollingParentForType(type);
		if (parent == null) {
			return false;
		}

		if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
		
			ViewParentCompat.onNestedScroll(parent, mView,
					dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
			return true;
		}
	}
	return false;
}

首先还是检查是否有能处理嵌套滑动的Parent,然后调用CoordinatorLayout.onNestedScroll()

public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
		int dxUnconsumed, int dyUnconsumed, int type) {
	final int childCount = getChildCount();
	boolean accepted = false;

	for (int i = 0; i < childCount; i++) {
		final View view = getChildAt(i);
		if (view.getVisibility() == GONE) {
			continue;
		}

		final LayoutParams lp = (LayoutParams) view.getLayoutParams();
		if (!lp.isNestedScrollAccepted(type)) {
			continue;
		}

		final Behavior viewBehavior = lp.getBehavior();
		if (viewBehavior != null) {
			viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
					dxUnconsumed, dyUnconsumed, type);
			accepted = true;
		}
	}

	if (accepted) {
		onChildViewsChanged(EVENT_NESTED_SCROLL);
	}
}

   遍历子View,对处于非GONE状态下的View获取它的Behavior,然后调用behavior.onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)默认还是空实现,自定义Behavior可以重写该方法去继续消耗RecyclerView没有消耗掉的滑动事件。
   嵌套滑动最后一个步骤-停止滑动的事件是在ACTION_UP事件中触发的,它的分发流程和大致相同从RecyclerView -> CoordinatorLayout -> Behavior。
   最后给出整体的嵌套滑动的流程图:
嵌套滑动事件传递流程

CoordinatorLayout内部View依赖关系

在自定义Behavior时有两个方法也经常用到:

  • public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency)
    决定child是否依赖于dependency
  • public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency)
    当dependency大小或者位置发生变化时child能接收到通知

在CoordinatorLayout的onMeasure方法内对它的子view进行一个依赖关系的计算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);

		// Now iterate again over the other children, adding any dependencies to the graph
		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);
			}
		}
	}

	// Finally add the sorted graph list to our list
	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);
}

假如CoordinatorLayout内部有4个TextView,它们有如下的依赖关系:
依赖关系
   在CoordinatorLayout内部先会以map<View, ArrayList< View>>建立依赖关系,然后通过深度优先算法进行遍历,结果存放于变量List< View> mDependencySortedChildren中,可以看到源码中对mDependencySortedChildren进行了一个逆序操作,这是为了保证第一个View一定是没有任何依赖的那一个,便于后续依赖关系事件的传递。
   维护好了上诉的依赖关系后,会在CoordinatorLayout的以下5个方法中都调用onChildViewsChanged(int type)函数:

  • type = EVENT_NESTED_SCROLL
  1. onNestedPreScroll() 滑动之前
  2. onNestedScroll() 滑动之后
  3. onNestedFling 惯性滑动之后
  • type = EVENT_PRE_DRAW
  1. OnPreDrawListener.onPreDraw() 在整个ViewTree准备绘制前
  • type = EVENT_VIEW_REMOVED
  1. HierarchyChangeListener.onChildViewRemoved() View从ViewTree中移除时

这几种情况View的位置和大小有可能发生改变,下边给出onChildViewsChanged()的源码:

//保留核心代码
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
	final int childCount = mDependencySortedChildren.size();

	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)) {

				final boolean handled;
				switch (type) {
					case EVENT_VIEW_REMOVED:
						b.onDependentViewRemoved(this, checkChild, child);
						handled = true;
						break;
					default:
						handled = b.onDependentViewChanged(this, checkChild, child);
						break;
				}

			}
		}
	}

}

   两层for循环,两两View之间通过调用b.layoutDependsOn(this, checkChild, child)判断是否有依赖关系,如果有那么通过b.onDependentViewChanged(this, checkChild, child)去通知对应的View,完成依赖关系的事件传递。在自定义Behavior中可以重写该方法,去实现相应的业务逻辑。

TODO

给出实际案例

CoordinatorLayoutAndroid Support Library中新增的一个布局容器,它继承自ViewGroup类。CoordinatorLayout的主要作用是协调子View之间的交互行为,可以让子View之间相互协调,实现一些比较复杂的交互效果。 1. 特点 - CoordinatorLayout可以作为根布局,内部可以包含多个子View。 - CoordinatorLayout可以通过设置各个子View之间的依赖关系,来实现各种复杂的交互效果。 - CoordinatorLayout默认会启用一个Behavior机制,通过设置Behavior可以对子View的布局和交互行为进行控制。 - CoordinatorLayout可以对子View进行移动、缩放、旋转等操作,实现各种炫酷的效果。 2. 使用 在使用CoordinatorLayout时,需要注意以下几点: - CoordinatorLayout必须作为根布局。 - 子View必须设置layout_behavior属性,用于指定Behavior。 - 子View之间可以通过设置app:layout_anchor属性和app:layout_anchorGravity属性来指定依赖关系。 - 子View的Behavior必须继承自CoordinatorLayout.Behavior类。 以下是一个简单的使用CoordinatorLayout的例子: ``` <android.support.design.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="match_parent" android:layout_height="200dp" android:src="@drawable/image" app:layout_behavior="@string/appbar_scrolling_view_behavior"/> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.design.widget.CollapsingToolbarLayout android:layout_width="match_parent" android:layout_height="200dp" app:layout_scrollFlags="scroll|exitUntilCollapsed"> <ImageView android:layout_width="match_parent" android:layout_height="200dp" android:src="@drawable/image" android:scaleType="centerCrop" app:layout_collapseMode="parallax"/> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.design.widget.FloatingActionButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/fab_margin" android:src="@drawable/ic_add" app:layout_anchor="@id/appbar" app:layout_anchorGravity="bottom|end"/> </android.support.design.widget.CoordinatorLayout> ``` 在这个例子中,CoordinatorLayout作为根布局,内部包含了一个ImageView、一个AppBarLayout和一个FloatingActionButton。通过设置各个View之间的依赖关系和Behavior,可以实现以下效果: - ImageView和AppBarLayout可以滚动。 - 当AppBarLayout滚动到顶部时,其中的Toolbar会固定在顶部。 - FloatingActionButton会跟随AppBarLayout一起滚动,并且会停留在AppBarLayout的底部。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值