一文帶你徹底搞懂Behavior實(shí)現(xiàn)復(fù)雜的視覺聯(lián)動效果原理
1、什么是 Behavior ?
Behavior 是谷歌 Material 設(shè)計(jì)中重要的一員,用來實(shí)現(xiàn)復(fù)雜的視覺聯(lián)動效果。
使用 Behavior 的控件需要被包裹在 CoordinateLayout 內(nèi)部。Behavior 就是一個(gè)接口。Behavior 實(shí)際上就是通過將 CoordinateLayout 的布局和觸摸事件傳遞給 Behavior 來實(shí)現(xiàn)的。
從設(shè)計(jì)模式上講,就一個(gè) Behavior 而言,它是一種訪問者模式,相當(dāng)于將 CoordinateLayout 的布局和觸摸過程對外提供的訪問器;而多個(gè) Behavior 在 CoordinateLayout 內(nèi)部的事件分發(fā)則是一種責(zé)任鏈機(jī)制,呈現(xiàn)出長幼有序的狀態(tài)。
以 layout 過程為例,
// androidx.coordinatorlayout.widget.CoordinatorLayout#onLayout
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) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
// 這里
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
可見 Behavior 就是將子控件的布局通過 onLayoutChild() 方法對外回調(diào)了出來??丶?behavior 優(yōu)先攔截和處理 layout 事件。
那 Behavior 相比于我們直接覆寫觸摸事件的形式處理手勢有什么優(yōu)點(diǎn)呢?
其優(yōu)點(diǎn)在于,我們可以將頁面的布局、觸摸和滑動等事件封裝到 Behavior 接口的實(shí)現(xiàn)類中以達(dá)到交互邏輯的復(fù)用和解耦的目的。
2、Behavior 接口的重要方法
Behavior 接口定義了許多方法,用于將 CoordinateLayout 的布局、測量和事件分發(fā)事件向外傳遞。這里我根據(jù)其作用將其歸納為以下幾組。
2.1 Behavior 生命周期相關(guān)的回調(diào)方法
首先是 Behavior 和 LayoutParams 關(guān)聯(lián)和接觸綁定時(shí)回調(diào)的方法。它們被回調(diào)的世紀(jì)分別是,
onAttachedToLayoutParams:LayoutParams 的構(gòu)造函數(shù)中回調(diào)onDetachedFromLayoutParams:調(diào)用 LayoutParams 的 setBehavior,用一個(gè)新的 Behavior 覆蓋舊的 Behavior 時(shí)回調(diào)
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {}
public void onDetachedFromLayoutParams() {}
2.2 子控件著色相關(guān)的回調(diào)方法
然后是跟 scrim color 相關(guān)的方法,這些方法會在 CoordinateLayout 的繪制過程中被調(diào)用。主要是跟繪制相關(guān)的,即用來對指定的 child 進(jìn)行著色。
這里的 child 是指該 Behavior 所關(guān)聯(lián)的控件,parent 就是指包裹這個(gè) child 的最外層的 CoordinatorLayout. 后面的方法都是如此。
public int getScrimColor(@NonNull CoordinatorLayout parent, @NonNull V child) public float getScrimOpacity(@NonNull CoordinatorLayout parent, @NonNull V child) public boolean blocksInteractionBelow(@NonNull CoordinatorLayout parent, @NonNull V child)
2.3 測量和布局相關(guān)的回調(diào)方法
然后一組方法是用來將 CoordinatorLayout 的測量和布局過程對外回調(diào)。不論是測量還是布局的回調(diào)方法,優(yōu)先級都是回調(diào)方法優(yōu)先。也就是回調(diào)方法可以通過返回 true 攔截 CoordinatorLayout 的邏輯。
另外,CoordinatorLayout 里使用 Behavior 的時(shí)候只會從直系子控件上讀取,所以,子控件的子控件上即便有 Behavior 也不會被攔截處理。所以,在一般使用 CoordinatorLayout 的時(shí)候,如果我們需要在某個(gè)控件上使用 Behavior,都是將其作為 CoordinatorLayout 的直系子控件。
還要注意,一個(gè) CoordinatorLayout 的直系子控件包含多個(gè) Behavior 的時(shí)候,這些 Behavior 被回調(diào)的先后順序和它們在 CoordinatorLayout 里布局的先后順序一致。也就是說,排序在前的子控件優(yōu)先攔截和處理事件。這和中國古代的王位繼承制差不多。
public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child, int parentWidthMeasureSpec,
int widthUsed, int parentHeightMeasureSpec, int heightUsed)
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection)
2.4 描述子控件之間依賴關(guān)系的回調(diào)
接下來的一組方法用來描述子控件之間的依賴關(guān)系。它的作用原理是,當(dāng) CoordinatorLayout 發(fā)生以下三類事件
- NestedScroll 滾動事件,通過
onNestedScroll()獲?。ê竺鏁治鲞@個(gè)事件工作原理) - PreDraw 事件,通過
ViewTreeObserver.OnPreDrawListener獲取到該事件 - 控件被移除事件,通過
OnHierarchyChangeListener獲取到該事件
的時(shí)候會使用 layoutDependsOn() 方法,針對 CoordinatorLayout 的每個(gè)子控件,判斷其他子控件與其是否構(gòu)成依賴關(guān)系。如果構(gòu)成了依賴關(guān)系,就回調(diào)其對應(yīng)的 Behavior 的 onDependentViewChanged() 或者 onDependentViewRemoved() 方法。
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency) public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency) public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)
2.5 與窗口變化和狀態(tài)保存與恢復(fù)相關(guān)的事件
然后是與窗口變化和狀態(tài)保存與恢復(fù)相關(guān)的事件。
public WindowInsetsCompat onApplyWindowInsets(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull WindowInsetsCompat insets)
public boolean onRequestChildRectangleOnScreen(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull Rect rectangle, boolean immediate)
public void onRestoreInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull Parcelable state)
public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child)
public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull Rect rect)
這些事件一般不會用到。
3、Behavior 的事件分發(fā)機(jī)制
以上是 Behavior 內(nèi)定義的一些方法。Behavior 主要的用途還是用來做觸摸事件的分發(fā)。這里,我們來重點(diǎn)關(guān)注和觸摸事件分發(fā)相關(guān)的方法。
3.1 安卓的觸摸事件分發(fā)機(jī)制
首先我們來回顧傳統(tǒng)的事件分發(fā)機(jī)制。當(dāng) window 將觸摸事件交給 DecorView 之后,觸摸事件在 ViewGroup 和 View 之間傳遞遵循如下模型,
// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
if ACTION_DOWN 事件并且 FLAG_DISALLOW_INTERCEPT 允許攔截 {
final boolean intercepted = onInterceptTouchEvent(ev) // 注意 onInterceptTouchEvent 的位置
}
boolean handled;
if !intercepted {
if child == null {
handled = super.dispatchTouchEvent(ev)
} else {
handled = child.dispatchTouchEvent(ev)
}
}
return handled;
}
// View
public boolean dispatchTouchEvent(MotionEvent event) {
if mOnTouchListener.onTouch(this, event) {
return true
}
if onTouchEvent(event) { // 注意 onTouchEvent 的位置
return true
}
return false
}
所以,子控件可以通過調(diào)用父控件的 requestDisallowInterceptTouchEvent() 方法不讓父控件攔截事件。但是這種攔截機(jī)制完全是基于默認(rèn)的實(shí)現(xiàn)邏輯。如果父控件修改了 requestDisallowInterceptTouchEvent() 方法或者 dispatchTouchEvent() 方法的邏輯,子控件的約束效果是無效的。
父控件通過 onInterceptTouchEvent() 攔截事件只能攔截部分事件。
相比于父控件,子控件的事件分發(fā)則簡單得多。首先是先將事件交給自定義的 mOnTouchListener 來處理,其沒有消費(fèi)才將其交給默認(rèn)的 onTouchEvent 來處理。在 onTouchEvent 里則會判斷事件的類型,比如點(diǎn)擊和長按之類的,而且可以看到系統(tǒng)源碼在判斷具體的事件類型的時(shí)候使用了 post Runnable 的方式。
在父控件中如果子控件沒有處理,則父控件將會走 View 的 dispatchTouchEvent() 邏輯,也就是去判斷事件的類型來消費(fèi)了。
3.2 與觸摸事件分發(fā)機(jī)制相關(guān)的方法
在 Behavior 中定義了兩個(gè)與觸摸事件分發(fā)相關(guān)的方法,
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev)
對照上面的事件分發(fā)機(jī)制中 onInterceptTouchEvent 和 onTouchEvent 的邏輯,這里的 Behavior 的攔截邏輯是:CoordinatorLayout 按照 Behavior 的出現(xiàn)順序進(jìn)行遍歷,先走 CoordinatorLayout 的 onInterceptTouchEvent,如果一個(gè) Behavior 的 onInterceptTouchEvent 攔截了該事件,則會記錄攔截該事件的 View 并給其他 Behavior 的 onInterceptTouchEvent 發(fā)送給一個(gè) Cancel 類型的觸摸事件。然后,在 CoordinatorLayout 的 onTouchEvent 方法中會執(zhí)行該 View 對應(yīng)的 Behavior 的 onTouchEvent 方法。
3.3 安卓的 NestedScrolling 機(jī)制
安卓在 5.0 上引入了 NestedScrolling 機(jī)制。之所以引入該事件是因?yàn)閭鹘y(tǒng)的事件分發(fā)機(jī)制 MOVE 事件當(dāng)父控件攔截了之后就無法再交給子 View. 而 NestedScrolling 機(jī)制可以指定在一個(gè)滑動事件中,父控件和子控件分別消費(fèi)多少。比如,在一個(gè)向上的滑動事件中,我們需要 toolbar 先向上滑動 50dp,然后列表再向上滑動。此時(shí),我們可以先讓 toolbar 消費(fèi) 50dp 的事件,剩下的再交給列表處理,讓其向上滑動 6dp 的距離。
在 NestedScrolling 機(jī)制中定義了 NestedScrollingChild 和 NestedScrollingParent 兩個(gè)接口(為了支持更多功能后續(xù)又定義了 NestedScrollingChild2 和 NestedScrollingChild3 等接口)。外部容器通常實(shí)現(xiàn) NestedScrollingParent 接口,而子控件通常實(shí)現(xiàn) NestedScrollingChild 接口。在常規(guī)的事件分發(fā)機(jī)制中,子控件(比如 RecyclerView 或者 NestedScrollView )會在 Move 事件中找到父控件,如果該父控件實(shí)現(xiàn)了 NestedScrollingParent 接口,就會通知該父控件發(fā)生了滑動事件。然后,父控件可以對滑動事件進(jìn)行進(jìn)一步的分發(fā)。以 RecyclerView 為例,
// androidx.recyclerview.widget.RecyclerView#onTouchEvent
public boolean onTouchEvent(MotionEvent e) {
// ...
switch (action) {
case MotionEvent.ACTION_MOVE: {
// ...
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
// ...
}
}
}
}
這里 dispatchNestedPreScroll() 就是滑動事件的分發(fā)邏輯,它最終會走到 ViewParentCompat 的 onNestedPreScroll() 方法,并在該方法中向上交給父控件進(jìn)行分發(fā)。代碼如下,
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
if (Build.VERSION.SDK_INT >= 21) {
parent.onNestedPreScroll(target, dx, dy, consumed);
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
}
}
}
3.4 與 NestedScrolling 相關(guān)的方法
在 CoordinatorLayout 中,與 NestedScrolling 機(jī)制相關(guān)的方法主要分成 scroll 和 fling 兩類。
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type)
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type)
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, @NestedScrollType int type)
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @NestedScrollType int type, @NonNull int[] consumed)
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type)
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY,
boolean consumed)
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY)
以 scroll 類型的事件為例,其工作的原理:
CoordinatorLayout 中會對子控件進(jìn)行遍歷,然后將對應(yīng)的事件傳遞給子控件的 Behavior (若有)的對應(yīng)方法。對于滑動類型的事件,在滑動事件傳遞的時(shí)候先傳遞 onStartNestedScroll 事件,用來判斷某個(gè) View 是否攔截滑動事件。而在 CoordinatorLayout 中,會交給 Beahvior 判斷是否處理該事件。然后 CoordinatorLayout 會講該 Behavior 是否攔截該事件的狀態(tài)記錄到對應(yīng)的 View 的 LayoutParam. 然后,當(dāng) CoordinatorLayout 的 onNestedPreScroll 被調(diào)用的時(shí)候,會讀取 LayoutParame 上的狀態(tài)以決定是否調(diào)用該 Behavior 的 onNestedPreScroll 方法。另外,只有當(dāng)一個(gè) CoordinatorLayout 包含的所有的 Behavior 都不處理該滑動事件的時(shí)候,才判定 CoordinatorLayout 不處理該滑動事件。
偽代碼如下,
// CoordinatorLayout
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
for 遍歷子 view {
Behavior viewBehavior = view.getLayoutParams().getBehavior()
final boolean accepted = viewBehavior.onStartNestedScroll();
handled |= accepted;
// 根據(jù) accepted 給 view 的 layoutparams 置位
view.getLayoutParams().setNestedScrollAccepted(accepted)
}
return handled;
}
// CoordinatorLayout
public void onStopNestedScroll(View target, int type) {
for 遍歷子 view {
// 讀取 view 的 layoutparams 的標(biāo)記位
if view.getLayoutParams().isNestedScrollAccepted(type) {
Behavior viewBehavior = view.getLayoutParams().getBehavior()
// 將事件交給 behavior
viewBehavior.onStopNestedScroll(this, view, target, type)
}
}
}
在消費(fèi)事件的時(shí)候是通過覆寫 onNestedPreScroll() 等方法,以該方法為例,
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {}
這里的 dx 和 dy 是滾動在水平和方向上的總的值,我們消費(fèi)的值通過 consumed 指定。比如 dy 表示向上一共滾動了 50dp,而我們的 toolbar 需要先向上滾動 44dp,那么我們就將 44dp 的數(shù)值賦值給 consumed 數(shù)組(方法簽名中的數(shù)組是按引用傳遞的)。這樣父控件就可以將剩下的 6dp 交給列表,所以列表最終會向上滾動 6dp.
3.5 觸摸事件分發(fā)機(jī)制小結(jié)
按照上述 Behavior 的實(shí)現(xiàn)方式,一個(gè) Behavior 是可以攔截到 CoordinatorLayout 內(nèi)所有的 View 的 NestedScrolling 事件的。因而,我們可以在一個(gè) Behavior 內(nèi)部對 CoordinatorLayout 內(nèi)的所有的 NestedScrolling 事件進(jìn)行統(tǒng)籌攔截和調(diào)度。用一個(gè)圖來表示整體分發(fā)邏輯,如下,

這里需要注意,按照我們上面的分析,CoordinatorLayout 收集到的事件 NestedScrolling 事件,如果一個(gè)控件并沒有實(shí)現(xiàn) NestedScrollingChild 接口,或者更嚴(yán)謹(jǐn)?shù)谜f,沒有將滾動事件傳遞給 CoordinatorLayout,那么 Behavior 就無法接受到滾動事件。但是對于普通的觸摸事件 Behavior 是可以攔截到的。
4、總結(jié)
這篇文章主要用來分析 Behavior 的整個(gè)工作原理。因?yàn)槠呀?jīng)比較長,這里就不再拿具體的案例進(jìn)行分析了。對于 Behavior,只要摸透了它是如何工作的,具體的案例分析起來也不會太難。
以上就是一文帶你徹底搞懂Behavior實(shí)現(xiàn)復(fù)雜的視覺聯(lián)動效果原理的詳細(xì)內(nèi)容,更多關(guān)于Behavior復(fù)雜視覺聯(lián)動的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)延遲的幾種方法小結(jié)
這篇文章主要介紹了Android實(shí)現(xiàn)延遲的幾種方法,結(jié)合實(shí)例總結(jié)了Android實(shí)現(xiàn)延遲的幾種常見技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-02-02
Android設(shè)備藍(lán)牙連接掃描槍獲取掃描內(nèi)容
這篇文章主要為大家詳細(xì)介紹了Android設(shè)備藍(lán)牙連接掃描槍獲取掃描內(nèi)容,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
Android中EditText setText方法的踩坑實(shí)戰(zhàn)
這篇文章主要給大家分享了一些關(guān)于Android中EditText setText方法的踩坑記錄,文中通過示例代碼介紹的非常詳細(xì),對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
android使用flutter的ListView實(shí)現(xiàn)滾動列表的示例代碼
現(xiàn)如今打開一個(gè) App,比如頭條、微博,都會有長列表,那么android使用flutter的ListView滾動列表如何實(shí)現(xiàn),本文就來詳細(xì)的介紹一下,感興趣的同學(xué)可以來了解一下2018-12-12
在Android設(shè)備上搭建Web服務(wù)器的方法
本篇文章主要介紹了在Android設(shè)備上搭建Web服務(wù)器的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04
Android測量每秒幀數(shù)Frames Per Second (FPS)的方法
這篇文章主要介紹了Android測量每秒幀數(shù)Frames Per Second (FPS)的方法,涉及Android針對多媒體文件屬性操作的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10
解決Kotlin 類在實(shí)現(xiàn)多個(gè)接口,覆寫多個(gè)接口中相同方法沖突的問題
這篇文章主要介紹了解決Kotlin 類在實(shí)現(xiàn)多個(gè)接口,覆寫多個(gè)接口中相同方法沖突的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android 控件(button)對齊方法實(shí)現(xiàn)詳解
horizontal是讓所有的子元素按水平方向從左到右排列,vertical是讓所有的子元素按豎直方向從上到下排列,下面為大家介紹下控件(button)的對齊方法2013-06-06
Android實(shí)現(xiàn)支付寶螞蟻森林水滴浮動效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)支付寶螞蟻森林水滴浮動效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06

