前言
許多文章都是將CoordinatorLayout、AppbarLayout、CollapsingToolbarLayout、Toolbar等放在一起介紹,容易誤解為這幾個(gè)布局一定要互相搭配,且僅僅適用于這些場(chǎng)景中。
其實(shí)不然,其中最重要的是CoordinatorLayout,我把它稱(chēng)為協(xié)調(diào)布局。協(xié)調(diào)什么布局呢?自然是嵌套在其內(nèi)部的 Child View。
CoordinatorLayout充當(dāng)了一個(gè)中間層的角色,一邊接收其他組件的事件,一邊將接收到的事件通知給內(nèi)部的其他組件。
Behavior就是CoordinatorLayout傳遞事件的媒介,Behavior 定義了 CoordinatorLayout 中**直接子 View **的行為規(guī)范,決定了當(dāng)收到不同事件時(shí),應(yīng)該做怎樣的處理。
總結(jié)來(lái)說(shuō),Behavior代理以下四種事件,其大致傳遞流程如下圖:

事件流好像很高深莫測(cè)的樣子...,再簡(jiǎn)化一點(diǎn)的說(shuō)法:CoordinatorLayout中的某個(gè)或某幾個(gè)方法被其他類(lèi)調(diào)用,之后CoordinatorLayout再調(diào)用Behavior中的某個(gè)或某幾個(gè)方法(=。=好像更抽象了)。總之,讓這四類(lèi)事件現(xiàn)在腦子里有個(gè)印象就可以了。
接著先介紹一下自定義Behavior的通用流程。為什么是通用流程呢?因?yàn)樯厦嫣岬搅擞兴姆N事件流,根據(jù)不同的事件流,是要重寫(xiě)不同的方法的,會(huì)在下面一一說(shuō)明。
自定義Behavior的通用流程
1. 重寫(xiě)構(gòu)造方法
public class CustomBehavior extends CoordinatorLayout.Behavior {
public CustomBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
一定要重寫(xiě)這個(gè)構(gòu)造方法,因?yàn)楫?dāng)你在XML中設(shè)置該Behavior時(shí),在 CoordinatorLayout 中會(huì)反射調(diào)用該方法,并生成該 Behavior 實(shí)例。
2. 綁定到View
綁定的方法有三種:
在 XML 文件中,設(shè)置任意 View 的屬性
app:layout_behavior="你的Behavior的包路徑和類(lèi)名"
或者在代碼中:
(CoordinatorLayout.LayoutParams)child.getLayoutParams().setBehavior();
再或者當(dāng)你的View是自定義的View時(shí)。
在你的自定義View類(lèi)上添加@DefaultBehavior(你的Behavior.class)。
@DefaultBehavior(CustomBehavior.class)
public class CustomView extends View {}
3. 判斷依賴(lài)對(duì)象
當(dāng) CoordinatorLayout 收到某個(gè) view 的變化或者嵌套滑動(dòng)事件時(shí),CoordinatorLayout就會(huì)嘗試把事件下發(fā)給Behavior,綁定了該 Behavior 的 view 就會(huì)對(duì)事件做出響應(yīng)。
下面是這兩個(gè)具有依賴(lài)的關(guān)系的view在Behavior方法中的形參名,方便讀者分辨:
被動(dòng)變化,也就是綁定了Behavior的view稱(chēng)為child
主動(dòng)變化的view在「變化事件」中稱(chēng)為dependency;在「嵌套滑動(dòng)事件」中稱(chēng)為target。
因?yàn)榭赡軙?huì)存在很多的Child View可以向CoordinatorLayout發(fā)出消息,也同時(shí)存在很多的Child View擁有著不同的Behavior,那么在CoordinatorLayout將真正的事件傳遞進(jìn)這個(gè)Behavior之前,肯定需要一個(gè)方法,告知CoordinatorLayout這兩者的依賴(lài)關(guān)系是否成立。如果關(guān)系成立,那么就把事件下發(fā)給你,如果關(guān)系不成立,那咱就到此over。
下面以「變化事件」的layoutDependsOn說(shuō)幾個(gè)例子,「嵌套滑動(dòng)事件」就在onStartNestedScroll中做同樣的判斷。另外的兩種「布局事件」「觸摸事件」就沒(méi)有這一步了。
a.根據(jù)id
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency.getId() == R.id.xxx;
}
b.根據(jù)類(lèi)型
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof CustomView;
}
c.根據(jù)id的另一種寫(xiě)法
<declare-styleable name="Follow">
<attr name="target" format="reference"/>
</declare-styleable>
先自定義target這個(gè)屬性。
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<View
android:id="@+id/first"
android:layout_width="match_parent"
android:layout_height="128dp"
android:background="@android:color/holo_blue_light"/>
<View
android:id="@+id/second"
android:layout_width="match_parent"
android:layout_height="128dp"
app:layout_behavior=".FollowBehavior"
app:target="@id/first"
android:background="@android:color/holo_green_light"/>
</android.support.design.widget.CoordinatorLayout>
public class FollowBehavior extends CoordinatorLayout.Behavior {
private int targetId;
public FollowBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Follow);
for (int i = 0; i < a.getIndexCount(); i++) {
int attr = a.getIndex(i);
if(a.getIndex(i) == R.styleable.Follow_target){
targetId = a.getResourceId(attr, -1);
}
}
a.recycle();
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
return true;
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency.getId() == targetId;
}
}
四種不同的事件流
1. 觸摸事件
TouchEvent 最主要的方法就是兩個(gè):
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent ev)
在 CoordinatorLayout 的 onInterceptTouchEvent 和 onTouchEvent 方法中,會(huì)嘗試調(diào)用其 Child View 擁有的 Behavior 中的同名方法。
public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev)
public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev)
如果 Behavior 對(duì)觸摸事件進(jìn)行了攔截,就不會(huì)再分發(fā)到 Child View 自身?yè)碛械挠|摸事件中。
這就意味著:在不知道具體View的情況下,就可以重寫(xiě)它的觸摸事件。
然而有一點(diǎn)我們需要注意到的是:onTouch事件是CoordinatorLayout分發(fā)下來(lái)的,所以這里的onTouchEvent并不是我們控件自己的onTouch事件,也就是說(shuō),你假如手指不在我們的控件上滑動(dòng),也會(huì)觸發(fā)onTouchEvent。
需要在onTouchEvent方法中的MotionEvent.ACTION_DOWN下添加:
ox = ev.getX();
oy = ev.getY();
if (oy < child.getTop() || oy > child.getBottom() || ox < child.getLeft() || ox > child.getRight()) {
return true;
}
對(duì)手勢(shì)的位置進(jìn)行過(guò)濾,不是我們控件范圍內(nèi)的,舍棄掉。
2. 布局事件
視圖布局無(wú)非就是這兩個(gè)方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected void onLayout(boolean changed, int l, int t, int r, int b)
在 CoordinatorLayout 的 onMeasure 和 onLayout 方法中,也會(huì)嘗試調(diào)用其 Child View 擁有的 Behavior 中對(duì)應(yīng)的方法,分別是:
public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed)
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection)
同樣地,CoordinatorLayout 會(huì)優(yōu)先處理 Behavior 中所重寫(xiě)的布局事件。
3. 變化事件
這個(gè)變化是指 View 的位置、尺寸發(fā)生了變化。
在 CoordinatorLayout 的 onDraw 方法中,會(huì)遍歷全部的 Child View 嘗試尋找是否有相互關(guān)聯(lián)的對(duì)象。
確定是否關(guān)聯(lián)的方式有兩種:
1. Behavior中定義
通過(guò) Behavior 的 layoutDependsOn 方法來(lái)判斷是否有依賴(lài)關(guān)系,如果有就繼續(xù)調(diào)用 onDependentViewChanged。FloatActionButton 可以在 Snackbar 彈出時(shí)主動(dòng)上移就通過(guò)該方式實(shí)現(xiàn)。
/**
* 判斷是dependency是否是當(dāng)前behavior需要的對(duì)象
* @param parent CoordinatorLayout
* @param child 該Behavior對(duì)應(yīng)的那個(gè)View
* @param dependency dependency 要檢查的View(child是否要依賴(lài)這個(gè)dependency)
* @return true 依賴(lài), false 不依賴(lài)
*/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
return false;
}
/**
* 當(dāng)改變dependency的尺寸或者位置時(shí)被調(diào)用
* @param parent CoordinatorLayout
* @param child 該Behavior對(duì)應(yīng)的那個(gè)View
* @param dependency child依賴(lài)dependency
* @return true 處理了, false 沒(méi)處理
*/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
return false;
}
/**
* 在layoutDependsOn返回true的基礎(chǔ)上之后,通知dependency被移除了
* @param parent CoordinatorLayout
* @param child 該Behavior對(duì)應(yīng)的那個(gè)View
* @param dependency child依賴(lài)dependency
*/
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, Button child, View dependency) {
}
2. XML中設(shè)置屬性
通過(guò) XML 中設(shè)置的 layout_anchor,關(guān)聯(lián)設(shè)置了 layout_anchor 的 Child View 與 layout_anchor 對(duì)應(yīng)的目標(biāo) dependency View。隨后調(diào)用 offsetChildToAnchor(child, layoutDirection);,其實(shí)就是調(diào)整兩者的位置,讓它們可以一起變化。FloatActionButton 可以跟隨 Toolbar 上下移動(dòng)就是該方式實(shí)現(xiàn)。
app:layout_anchor="@id/dependencyView.id"
4. 嵌套滑動(dòng)事件
實(shí)現(xiàn)NestedScrollingChild
如果一個(gè)View想向外界傳遞滑動(dòng)事件,即通知 NestedScrollingParent ,就必須實(shí)現(xiàn)此接口。
而 Child 與 Parent 的具體交互邏輯, NestedScrollingChildHelper 輔助類(lèi)基本已經(jīng)幫我們封裝好了,所以我們只需要調(diào)用對(duì)應(yīng)的方法即可。
NestedScrollingChild接口的一般實(shí)現(xiàn):
public class CustomNestedScrollingChildView extends View implements NestedScrollingChild {
private NestedScrollingChildHelper mChildHelper = new NestedScrollingChildHelper(this);
/**
* 設(shè)置當(dāng)前View能否滑動(dòng)
* @param enabled
*/
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
/**
* 判斷當(dāng)前View能否滑動(dòng)
* @return
*/
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
/**
* 啟動(dòng)嵌套滑動(dòng)事件流
* 1. 尋找可以接收 NestedScroll 事件的 parent view,即實(shí)現(xiàn)了 NestedScrollingParent 接口的 ViewGroup
* 2. 通知該 parent view,現(xiàn)在我要把滑動(dòng)的參數(shù)傳遞給你
* @param axes
* @return
*/
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
/**
* 停止嵌套滑動(dòng)事件流
*/
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
/**
* 是否存在接收 NestedScroll 事件的 parent view
* @return
*/
@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}
/**
* 在滑動(dòng)之后,向父view匯報(bào)滾動(dòng)情況,包括child view消費(fèi)的部分和child view沒(méi)有消費(fèi)的部分。
* @param dxConsumed x方向已消費(fèi)的滑動(dòng)距離
* @param dyConsumed y方向已消費(fèi)的滑動(dòng)距離
* @param dxUnconsumed x方向未消費(fèi)的滑動(dòng)距離
* @param dyUnconsumed y方向未消費(fèi)的滑動(dòng)距離
* @param offsetInWindow 如果parent view滑動(dòng)導(dǎo)致child view的窗口發(fā)生了變化(child View的位置發(fā)生了變化)
* 該參數(shù)返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化
* 如果你記錄了手指最后的位置,需要根據(jù)參數(shù)offsetInWindow計(jì)算偏移量,
* 才能保證下一次的touch事件的計(jì)算是正確的。
* @return 如果parent view接受了它的滾動(dòng)參數(shù),進(jìn)行了部分消費(fèi),則這個(gè)函數(shù)返回true,否則為false。
*/
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow);
}
/**
* 在滑動(dòng)之前,先問(wèn)一下 parent view 是否需要滑動(dòng),
* 即child view的onInterceptTouchEvent或onTouchEvent方法中調(diào)用。
* 1. 如果parent view滑動(dòng)了一定距離,你需要重新計(jì)算一下parent view滑動(dòng)后剩下給你的滑動(dòng)距離剩余量,
* 然后自己進(jìn)行剩余的滑動(dòng)。
* 2. 該方法的第三第四個(gè)參數(shù)返回parent view消費(fèi)掉的滑動(dòng)距離和child view的窗口偏移量,
* 如果你記錄了手指最后的位置,需要根據(jù)第四個(gè)參數(shù)offsetInWindow計(jì)算偏移量,
* 才能保證下一次的touch事件的計(jì)算是正確的。
* @param dx x方向的滑動(dòng)距離
* @param dy y方向的滑動(dòng)距離
* @param consumed 如果不是null, 則告訴child view現(xiàn)在parent view滑動(dòng)的情況,
* consumed[0]parent view告訴child view水平方向滑動(dòng)的距離(dx)
* consumed[1]parent view告訴child view垂直方向滑動(dòng)的距離(dy)
* @param offsetInWindow 可選 length=2 的數(shù)組,
* 如果parent view滑動(dòng)導(dǎo)致child View的窗口發(fā)生了變化(子View的位置發(fā)生了變化)
* 該參數(shù)返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化
* 如果你記錄了手指最后的位置,需要根據(jù)參數(shù)offsetInWindow計(jì)算偏移量,
* 才能保證下一次的touch事件的計(jì)算是正確的。
* @return 如果parent view對(duì)滑動(dòng)距離進(jìn)行了部分消費(fèi),則這個(gè)函數(shù)返回true,否則為false。
*/
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
/**
* 在嵌套滑動(dòng)的child view快速滑動(dòng)之后再調(diào)用該函數(shù)向parent view匯報(bào)快速滑動(dòng)情況。
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @param consumed true 表示child view快速滑動(dòng)了, false 表示child view沒(méi)有快速滑動(dòng)
* @return true 表示parent view快速滑動(dòng)了, false 表示parent view沒(méi)有快速滑動(dòng)
*/
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
/**
* 在嵌套滑動(dòng)的child view快速滑動(dòng)之前告訴parent view快速滑動(dòng)的情況。
* @param velocityX 水平方向的速度
* @param velocityY 垂直方向的速度
* @return true 表示parent view快速滑動(dòng)了, false 表示parent view沒(méi)有快速滑動(dòng)
*/
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
實(shí)現(xiàn)NestedScrollingParent
如果一個(gè)View Group想接收來(lái)自 NestedScrollingChild 的滑動(dòng)事件,就需要實(shí)現(xiàn)該接口。
同樣有一個(gè) NestedScrollingParentHelper
輔助類(lèi),幫我們封裝好了 parent view 與 child view之間的具體交互邏輯。
由 NestedScrollingChild 主動(dòng)發(fā)出滑動(dòng)事件傳遞給 NestedScrollingParent,NestedScrollingParent 做出響應(yīng)。
之間的調(diào)用關(guān)系如下表所示:
| Child View | Parent View |
|---|---|
| startNestedScroll | onStartNestedScroll、onNestedScrollAccepted |
| dispatchNestedPreScroll | onNestedPreScroll |
| dispatchNestedScroll | onNestedScroll |
| stopNestedScroll | onStopNestedScroll |
| dispatchNestedFling | onNestedFling |
| dispatchNestedPreFling | onNestedPreFling |
繼承Behavior
在上面的說(shuō)明中提到 Parent View 會(huì)消費(fèi)一部分或全部的滑動(dòng)距離,但其實(shí)大部分情況下,我們的 Parent View 自身并不會(huì)消費(fèi)滑動(dòng)距離,都是傳遞給 Behavior,也就是擁有這個(gè) Behavior 的 Child View 才是真正消費(fèi)滑動(dòng)距離的實(shí)例。
Behavior 擁有與 NestedScrollingParent 接口完全同名的方法。在每一個(gè) NestedScrollingParent 的方法中都會(huì)調(diào)用 Behavior 中的同名方法。
有這么幾個(gè)方法做下特別說(shuō)明:
/**
* 開(kāi)始嵌套滑動(dòng)的時(shí)候被調(diào)用
* 1. 需要判斷滑動(dòng)的方向是否是我們需要的。
* nestedScrollAxes == ViewCompat.SCROLL_AXIS_HORIZONTAL 表示是水平方向的滑動(dòng)
* nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL 表示是豎直方向的滑動(dòng)
* 2. 返回 true 表示繼續(xù)接收后續(xù)的滑動(dòng)事件,返回 false 表示不再接收后續(xù)滑動(dòng)事件
*/
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
View directTargetChild, View target, int nestedScrollAxes) {
}
/**
* 滑動(dòng)中調(diào)用
* 1. 正在上滑:dyConsumed > 0 && dyUnconsumed == 0
* 2. 已經(jīng)到頂部了還在上滑:dyConsumed == 0 && dyUnconsumed > 0
* 3. 正在下滑:dyConsumed < 0 && dyUnconsumed == 0
* 4. 已經(jīng)打底部了還在下滑:dyConsumed == 0 && dyUnconsumed < 0
*/
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
/**
* 快速滑動(dòng)中調(diào)用
*/
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
}
總結(jié)
總結(jié)一下這四種事件流,和各自需要實(shí)現(xiàn)的方法。
根據(jù)在自定義Behavior時(shí)是否需要判斷依賴(lài)關(guān)系,把Behavior代理的四種情況分成兩類(lèi):
事件來(lái)自外部父view:
1.布局事件:Behavior的 onMeasureChild+onLayoutChild
2.觸摸事件:Behavior的onInterceptTouchEvent+onTouchEvent
事件來(lái)自?xún)?nèi)部子view:
3.view變化事件:Behavior的layoutDependsOn+onDependentViewChanged+onDependentViewRemoved
4.嵌套滑動(dòng)事件:Behavior的onStartNestedScroll+onNestedScrollAccepted+onStopNestedScroll+onNestedScroll+onNestedPreScroll+onNestedFling+onNestedPreFling
后記
之前在Google、百度自定義Behavior造輪子的時(shí)候,剛開(kāi)始看一篇,覺(jué)得不過(guò)如此,就這么點(diǎn)東西。再看一篇,咦~實(shí)現(xiàn)怎么又不一樣了,再來(lái)一篇又不一樣了。
本文就是想起一個(gè)大綱的作用,輪子再怎么造,還是這么些個(gè)方法。以后再看別人的輪子或者自己造輪子的時(shí)候,可以清晰一些。
擴(kuò)展
sidhu眼中的CoordinatorLayout.Behavior(一)
sidhu眼中的CoordinatorLayout.Behavior(二)
sidhu眼中的CoordinatorLayout.Behavior(三)
Material Design系列,自定義Behavior支持所有View
CoordinatorLayout的使用如此簡(jiǎn)單