最近因為需要研究一個滑動懸浮效果巴柿,偶然間發(fā)現(xiàn)了CoordinatorLayout這個很強大的布局雌贱,這個控件一般需要配合AppBarLayout啊送、CollapsingToolbarLayout使用來實現(xiàn)一些懸浮和漸變的高級效果,相關(guān)的使用文章有很多欣孤,這篇就不介紹這些了馋没,寫這篇的主要目的是要記錄一個問題,給CoordinatorLayout的子View設(shè)置Behavior后降传,Behavior 的layoutDependsOn和onDependentViewChanged方法是CoordinatorLayout何時進行回調(diào)來達到協(xié)調(diào)的目的的篷朵。
如果不是太了解CoordinatorLayout可以先看一下這兩篇文章:
CoordinatorLayout (這篇介紹了CoordinatorLayout 最基本的一個使用方式)
一步一步深入理解CoordinatorLayout( 這篇介紹了一下部分代碼)
下面開始我的分析和記錄
創(chuàng)建&&使用自定義的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控件有相關(guān)變化時婆排,就會回調(diào)Behavior中的方法声旺,來改變我們的childView
何時回調(diào)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();
//在這里將實現(xiàn)了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的一個內(nèi)部類OnPreDrawListener實現(xiàn)了ViewTreeObserver.OnPreDrawListener澈缺,然后注冊到了ViewTreeObserver上,OnPreDrawListener是ViewTreeObserver上的一個回調(diào)接口內(nèi)部聲明如下:
/**
* 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準備繪制時回調(diào)炕婶,可以利用這個方法在繪制發(fā)生之前去調(diào)整滾動的邊界或者去請求一個新的layout姐赡,所以CoordinatorLayout就是在onPreDraw()方法中回調(diào)我們Behavior中的方法,具體的調(diào)用方法就是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++) {
//獲取根據(jù)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()
// 在這里回調(diào)了Behavior的b.onDependentViewChanged方法來通知ChildView的dependency發(fā)生了改變
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 設(shè)置了OnPreDrawListener會在重新繪制前通知CoordinatorLayout肩狂,CoordinatorLayout在通過調(diào)用onChildViewsChanged來遍歷子View摘完,因為子View已經(jīng)經(jīng)過排序傻谁,遍歷到每一個子View時孝治,會在去遍歷當前這個子View之后的View,過程如下:
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
//省略具體代碼
}
然后在遍歷到每一個i+1位置開始時的子View時岂座,會獲取這個子View 的LayoutParams,然后調(diào)用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泉懦,才會回調(diào)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()
// 在這里回調(diào)了Behavior的b.onDependentViewChanged方法來通知ChildView的dependency發(fā)生了改變
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中的實現(xiàn):
@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钉嘹,然后會回調(diào)onDependentViewChanged鸯乃,我可以在拿到Button child, View dependency后,可以做一些變化操作鸟悴,例如開頭推薦閱讀的第一篇文章中的效果。
最后奖年,其實onChildViewsChanged并不只是在onPreDraw中才會回調(diào),通過入?yún)⑽覀兛梢钥吹秸鸸螅琽nChildViewsChanged需要傳入一個@DispatchChangeEvent final int type的參數(shù)水评,這個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)