CoordinatorLayout Behavior一些笔记
最近因为需要研究一个滑动悬浮效果,偶然间发现了CoordinatorLayout这个很强大的布局,这个控件一般需要配合AppBarLayout、CollapsingToolbarLayout使用来实现一些悬浮和渐变的高级效果,相关的使用文章有很多,这篇就不介绍这些了,写这篇的主要目的是要记录一个问题,给CoordinatorLayout的子View设置Behavior后,Behavior 的layoutDependsOn和onDependentViewChanged方法是CoordinatorLayout何时进行回调来达到协调的目的的。
如果不是太了解CoordinatorLayout可以先看一下这两篇文章:
CoordinatorLayout (这篇介绍了CoordinatorLayout 最基本的一个使用方式)
一步一步深入理解CoordinatorLayout( 这篇介绍了一下部分代码)
下面开始我的分析和记录
创建&&使用自定义的Behavior
- 当我们想自定义Behavior时需要继承CoordinatorLayout.Behavior<V extends View> ,例如我定义了如下Behavior
public class MyBehavior extends CoordinatorLayout.Behavior<Button> {
public MyBehavior(Context context, AttributeSet attrs){
super(context,attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
return dependency instanceof TestTextView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
//do something
return super.onDependentViewChanged(parent, child, dependency);
}
}
然后当我使用时我可以在xml中定义一个Button类型的ChildView使用这个Behavior
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="300dp"
android:layout_marginTop="300dp"
android:background="#FFCC00"
android:text="Hello"
app:layout_behavior="com.humorous.myapplication.coordinatorTest.behavior.MyBehavior"/>
<com.humorous.myapplication.coordinatorTest.widget.TestTextView
android:id="@+id/textView"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginLeft="300dp"
android:layout_marginTop="300dp"
android:background="#3366CC" />
</android.support.design.widget.CoordinatorLayout>
这样当我UI中TestTextView控件有相关变化时,就会回调Behavior中的方法,来改变我们的childView
何时回调layoutDependsOn 和onDependentViewChanged?
- 这里才是我这篇文章想要记录的重点,我很好奇,我的dependency View改变时CoordinatorLayout是怎么通知我的Behavior的,这里就需要贴一些源码了
private OnPreDrawListener mOnPreDrawListener;
....
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors();
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
//在这里将实现了OnPreDrawListener的对象注册到ViewTreeObserver中
vto.addOnPreDrawListener(mOnPreDrawListener);
}
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
// We're set to fitSystemWindows but we haven't had any insets yet...
// We should request a new dispatch of window insets
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
....
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
上面的代码表明,CoordinatorLayout的一个内部类OnPreDrawListener实现了ViewTreeObserver.OnPreDrawListener,然后注册到了ViewTreeObserver上,OnPreDrawListener是ViewTreeObserver上的一个回调接口内部声明如下:
/**
* Interface definition for a callback to be invoked when the view tree is about to be drawn.
*/
public interface OnPreDrawListener {
/**
* Callback method to be invoked when the view tree is about to be drawn. At this point, all
* views in the tree have been measured and given a frame. Clients can use this to adjust
* their scroll bounds or even to request a new layout before drawing occurs.
*
* @return Return true to proceed with the current drawing pass, or false to cancel.
*
* @see android.view.View#onMeasure
* @see android.view.View#onLayout
* @see android.view.View#onDraw
*/
public boolean onPreDraw();
}
这个接口会在viewTree准备绘制时回调,可以利用这个方法在绘制发生之前去调整滚动的边界或者去请求一个新的layout,所以CoordinatorLayout就是在onPreDraw()方法中回调我们Behavior中的方法,具体的调用方法就是CoordinatorLayout 中onChildViewsChanged,该方法的代码如下(省略部分):
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++) {
//获取根据z轴排序的子View
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
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
// Get the current draw rect of the view
getChildRect(child, true, drawRect);
// Accumulate inset sizes
....//省略部分代码
// Dodge inset edges if necessary
....//省略部分代码
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
//获取i+1位置开始的ChildView
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
//获取Child的Behavior
final Behavior b = checkLp.getBehavior();
//Child的Behavior不为空,并且Behavior的b.layoutDependsOn返回了true
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
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()
// 在这里回调了Behavior的b.onDependentViewChanged方法来通知ChildView的dependency发生了改变
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
releaseTempRect(inset);
releaseTempRect(drawRect);
releaseTempRect(lastDrawRect);
}
看到这个方法,我心中的疑惑就解开了,当子View改变时,会引起ViewTree重新绘制,然后因为CoordinatorLayout 设置了OnPreDrawListener会在重新绘制前通知CoordinatorLayout,CoordinatorLayout在通过调用onChildViewsChanged来遍历子View,因为子View已经经过排序,遍历到每一个子View时,会在去遍历当前这个子View之后的View,过程如下:
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
//省略具体代码
}
然后在遍历到每一个i+1位置开始时的子View时,会获取这个子View 的LayoutParams,然后调用getBehavior方法获取Behavior,过程如下:
for (int j = i + 1; j < childCount; j++) {
//获取i+1位置开始的ChildView
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
//获取Child的Behavior
final Behavior b = checkLp.getBehavior();
//省略后面的代码
}
在拿到这个Behavior后,如果这个Behavior不为空,并且Behavior的layoutDependsOn返回了true,代表j位置的子View依赖于i位置的子View,才会回调Behavior的onDependentViewChanged,过程如下:
// Update any behavior-dependent views for the change
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();
//Child的Behavior不为空,并且Behavior的b.layoutDependsOn返回了true
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的b.onDependentViewChanged方法来通知ChildView的dependency发生了改变
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
我自定的Behavior中的实现:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
return dependency instanceof TestTextView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
//do something
return super.onDependentViewChanged(parent, child, dependency);
}
这样此时的i位置的子View是TestTextView类型时,我的layoutDependsOn会返回true,然后会回调onDependentViewChanged,我可以在拿到Button child, View dependency后,可以做一些变化操作,例如开头推荐阅读的第一篇文章中的效果。
最后,其实onChildViewsChanged并不只是在onPreDraw中才会回调,通过入参我们可以看到,onChildViewsChanged需要传入一个@DispatchChangeEvent final int type的参数,这个type一共有三种类型
static final int EVENT_PRE_DRAW = 0;
static final int EVENT_NESTED_SCROLL = 1;
static final int EVENT_VIEW_REMOVED = 2;
最后我们可以通过查看onChildViewsChanged方法前面的描述来知道它的作用到底是在做什么,这里我只把这个描述贴出来,就不做翻译了,因为我的翻译水平有限,会破坏了原有的意境
/**
* Dispatch any dependent view changes to the relevant {@link Behavior} instances.
*
* Usually run as part of the pre-draw step when at least one child view has a reported
* dependency on another view. This allows CoordinatorLayout to account for layout
* changes and animations that occur outside of the normal layout pass.
*
* It can also be ran as part of the nested scrolling dispatch to ensure that any offsetting
* is completed within the correct coordinate window.
*
* The offsetting behavior implemented here does not store the computed offset in
* the LayoutParams; instead it expects that the layout process will always reconstruct
* the proper positioning.
*
* @param type the type of event which has caused this call
*/
final void onChildViewsChanged(@DispatchChangeEvent final int type)