一文帶你徹底搞懂Behavior實(shí)現(xiàn)復(fù)雜的視覺(jué)聯(lián)動(dòng)效果原理
1、什么是 Behavior ?
Behavior 是谷歌 Material 設(shè)計(jì)中重要的一員,用來(lái)實(shí)現(xiàn)復(fù)雜的視覺(jué)聯(lián)動(dòng)效果。
使用 Behavior 的控件需要被包裹在 CoordinateLayout 內(nèi)部。Behavior 就是一個(gè)接口。Behavior 實(shí)際上就是通過(guò)將 CoordinateLayout 的布局和觸摸事件傳遞給 Behavior 來(lái)實(shí)現(xiàn)的。
從設(shè)計(jì)模式上講,就一個(gè) Behavior 而言,它是一種訪問(wèn)者模式,相當(dāng)于將 CoordinateLayout 的布局和觸摸過(guò)程對(duì)外提供的訪問(wèn)器;而多個(gè) Behavior 在 CoordinateLayout 內(nèi)部的事件分發(fā)則是一種責(zé)任鏈機(jī)制,呈現(xiàn)出長(zhǎng)幼有序的狀態(tài)。
以 layout 過(guò)程為例,
// 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 就是將子控件的布局通過(guò) onLayoutChild()
方法對(duì)外回調(diào)了出來(lái)??丶?behavior 優(yōu)先攔截和處理 layout 事件。
那 Behavior 相比于我們直接覆寫觸摸事件的形式處理手勢(shì)有什么優(yōu)點(diǎn)呢?
其優(yōu)點(diǎn)在于,我們可以將頁(yè)面的布局、觸摸和滑動(dòng)等事件封裝到 Behavior 接口的實(shí)現(xiàn)類中以達(dá)到交互邏輯的復(fù)用和解耦的目的。
2、Behavior 接口的重要方法
Behavior 接口定義了許多方法,用于將 CoordinateLayout 的布局、測(cè)量和事件分發(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)的方法,這些方法會(huì)在 CoordinateLayout 的繪制過(guò)程中被調(diào)用。主要是跟繪制相關(guān)的,即用來(lái)對(duì)指定的 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 測(cè)量和布局相關(guān)的回調(diào)方法
然后一組方法是用來(lái)將 CoordinatorLayout 的測(cè)量和布局過(guò)程對(duì)外回調(diào)。不論是測(cè)量還是布局的回調(diào)方法,優(yōu)先級(jí)都是回調(diào)方法優(yōu)先。也就是回調(diào)方法可以通過(guò)返回 true 攔截 CoordinatorLayout 的邏輯。
另外,CoordinatorLayout 里使用 Behavior 的時(shí)候只會(huì)從直系子控件上讀取,所以,子控件的子控件上即便有 Behavior 也不會(huì)被攔截處理。所以,在一般使用 CoordinatorLayout 的時(shí)候,如果我們需要在某個(gè)控件上使用 Behavior,都是將其作為 CoordinatorLayout 的直系子控件。
還要注意,一個(gè) CoordinatorLayout 的直系子控件包含多個(gè) Behavior 的時(shí)候,這些 Behavior 被回調(diào)的先后順序和它們?cè)?CoordinatorLayout 里布局的先后順序一致。也就是說(shuō),排序在前的子控件優(yōu)先攔截和處理事件。這和中國(guó)古代的王位繼承制差不多。
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)
接下來(lái)的一組方法用來(lái)描述子控件之間的依賴關(guān)系。它的作用原理是,當(dāng) CoordinatorLayout 發(fā)生以下三類事件
- NestedScroll 滾動(dòng)事件,通過(guò)
onNestedScroll()
獲取(后面會(huì)分析這個(gè)事件工作原理) - PreDraw 事件,通過(guò)
ViewTreeObserver.OnPreDrawListener
獲取到該事件 - 控件被移除事件,通過(guò)
OnHierarchyChangeListener
獲取到該事件
的時(shí)候會(huì)使用 layoutDependsOn()
方法,針對(duì) CoordinatorLayout 的每個(gè)子控件,判斷其他子控件與其是否構(gòu)成依賴關(guān)系。如果構(gòu)成了依賴關(guān)系,就回調(diào)其對(duì)應(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)
這些事件一般不會(huì)用到。
3、Behavior 的事件分發(fā)機(jī)制
以上是 Behavior 內(nèi)定義的一些方法。Behavior 主要的用途還是用來(lái)做觸摸事件的分發(fā)。這里,我們來(lái)重點(diǎn)關(guān)注和觸摸事件分發(fā)相關(guān)的方法。
3.1 安卓的觸摸事件分發(fā)機(jī)制
首先我們來(lái)回顧傳統(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 }
所以,子控件可以通過(guò)調(diào)用父控件的 requestDisallowInterceptTouchEvent()
方法不讓父控件攔截事件。但是這種攔截機(jī)制完全是基于默認(rèn)的實(shí)現(xiàn)邏輯。如果父控件修改了 requestDisallowInterceptTouchEvent()
方法或者 dispatchTouchEvent()
方法的邏輯,子控件的約束效果是無(wú)效的。
父控件通過(guò) onInterceptTouchEvent()
攔截事件只能攔截部分事件。
相比于父控件,子控件的事件分發(fā)則簡(jiǎn)單得多。首先是先將事件交給自定義的 mOnTouchListener 來(lái)處理,其沒(méi)有消費(fèi)才將其交給默認(rèn)的 onTouchEvent 來(lái)處理。在 onTouchEvent 里則會(huì)判斷事件的類型,比如點(diǎn)擊和長(zhǎng)按之類的,而且可以看到系統(tǒng)源碼在判斷具體的事件類型的時(shí)候使用了 post Runnable 的方式。
在父控件中如果子控件沒(méi)有處理,則父控件將會(huì)走 View 的 dispatchTouchEvent()
邏輯,也就是去判斷事件的類型來(lái)消費(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)
對(duì)照上面的事件分發(fā)機(jī)制中 onInterceptTouchEvent 和 onTouchEvent 的邏輯,這里的 Behavior 的攔截邏輯是:CoordinatorLayout 按照 Behavior 的出現(xiàn)順序進(jìn)行遍歷,先走 CoordinatorLayout 的 onInterceptTouchEvent,如果一個(gè) Behavior 的 onInterceptTouchEvent 攔截了該事件,則會(huì)記錄攔截該事件的 View 并給其他 Behavior 的 onInterceptTouchEvent 發(fā)送給一個(gè) Cancel 類型的觸摸事件。然后,在 CoordinatorLayout 的 onTouchEvent 方法中會(huì)執(zhí)行該 View 對(duì)應(yīng)的 Behavior 的 onTouchEvent 方法。
3.3 安卓的 NestedScrolling 機(jī)制
安卓在 5.0 上引入了 NestedScrolling 機(jī)制。之所以引入該事件是因?yàn)閭鹘y(tǒng)的事件分發(fā)機(jī)制 MOVE 事件當(dāng)父控件攔截了之后就無(wú)法再交給子 View. 而 NestedScrolling 機(jī)制可以指定在一個(gè)滑動(dòng)事件中,父控件和子控件分別消費(fèi)多少。比如,在一個(gè)向上的滑動(dòng)事件中,我們需要 toolbar 先向上滑動(dòng) 50dp,然后列表再向上滑動(dòng)。此時(shí),我們可以先讓 toolbar 消費(fèi) 50dp 的事件,剩下的再交給列表處理,讓其向上滑動(dòng) 6dp 的距離。
在 NestedScrolling 機(jī)制中定義了 NestedScrollingChild
和 NestedScrollingParent
兩個(gè)接口(為了支持更多功能后續(xù)又定義了 NestedScrollingChild2 和 NestedScrollingChild3 等接口)。外部容器通常實(shí)現(xiàn) NestedScrollingParent
接口,而子控件通常實(shí)現(xiàn) NestedScrollingChild
接口。在常規(guī)的事件分發(fā)機(jī)制中,子控件(比如 RecyclerView 或者 NestedScrollView )會(huì)在 Move 事件中找到父控件,如果該父控件實(shí)現(xiàn)了 NestedScrollingParent
接口,就會(huì)通知該父控件發(fā)生了滑動(dòng)事件。然后,父控件可以對(duì)滑動(dòng)事件進(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()
就是滑動(dòng)事件的分發(fā)邏輯,它最終會(huì)走到 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 中會(huì)對(duì)子控件進(jìn)行遍歷,然后將對(duì)應(yīng)的事件傳遞給子控件的 Behavior (若有)的對(duì)應(yīng)方法。對(duì)于滑動(dòng)類型的事件,在滑動(dòng)事件傳遞的時(shí)候先傳遞 onStartNestedScroll 事件,用來(lái)判斷某個(gè) View 是否攔截滑動(dòng)事件。而在 CoordinatorLayout 中,會(huì)交給 Beahvior 判斷是否處理該事件。然后 CoordinatorLayout 會(huì)講該 Behavior 是否攔截該事件的狀態(tài)記錄到對(duì)應(yīng)的 View 的 LayoutParam. 然后,當(dāng) CoordinatorLayout 的 onNestedPreScroll 被調(diào)用的時(shí)候,會(huì)讀取 LayoutParame 上的狀態(tài)以決定是否調(diào)用該 Behavior 的 onNestedPreScroll 方法。另外,只有當(dāng)一個(gè) CoordinatorLayout 包含的所有的 Behavior 都不處理該滑動(dòng)事件的時(shí)候,才判定 CoordinatorLayout 不處理該滑動(dòng)事件。
偽代碼如下,
// 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í)候是通過(guò)覆寫 onNestedPreScroll()
等方法,以該方法為例,
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {}
這里的 dx 和 dy 是滾動(dòng)在水平和方向上的總的值,我們消費(fèi)的值通過(guò) consumed 指定。比如 dy 表示向上一共滾動(dòng)了 50dp,而我們的 toolbar 需要先向上滾動(dòng) 44dp,那么我們就將 44dp 的數(shù)值賦值給 consumed 數(shù)組(方法簽名中的數(shù)組是按引用傳遞的)。這樣父控件就可以將剩下的 6dp 交給列表,所以列表最終會(huì)向上滾動(dòng) 6dp.
3.5 觸摸事件分發(fā)機(jī)制小結(jié)
按照上述 Behavior 的實(shí)現(xiàn)方式,一個(gè) Behavior 是可以攔截到 CoordinatorLayout 內(nèi)所有的 View 的 NestedScrolling 事件的。因而,我們可以在一個(gè) Behavior 內(nèi)部對(duì) CoordinatorLayout 內(nèi)的所有的 NestedScrolling 事件進(jìn)行統(tǒng)籌攔截和調(diào)度。用一個(gè)圖來(lái)表示整體分發(fā)邏輯,如下,
這里需要注意,按照我們上面的分析,CoordinatorLayout 收集到的事件 NestedScrolling 事件,如果一個(gè)控件并沒(méi)有實(shí)現(xiàn) NestedScrollingChild 接口,或者更嚴(yán)謹(jǐn)?shù)谜f(shuō),沒(méi)有將滾動(dòng)事件傳遞給 CoordinatorLayout,那么 Behavior 就無(wú)法接受到滾動(dòng)事件。但是對(duì)于普通的觸摸事件 Behavior 是可以攔截到的。
4、總結(jié)
這篇文章主要用來(lái)分析 Behavior 的整個(gè)工作原理。因?yàn)槠呀?jīng)比較長(zhǎng),這里就不再拿具體的案例進(jìn)行分析了。對(duì)于 Behavior,只要摸透了它是如何工作的,具體的案例分析起來(lái)也不會(huì)太難。
以上就是一文帶你徹底搞懂Behavior實(shí)現(xiàn)復(fù)雜的視覺(jué)聯(lián)動(dòng)效果原理的詳細(xì)內(nèi)容,更多關(guān)于Behavior復(fù)雜視覺(jué)聯(lián)動(dòng)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android入門之使用SharedPreference存取信息詳解
這篇文章主要為大家詳細(xì)介紹了Android如何使用SharedPreference實(shí)現(xiàn)存取信息,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Android有一定的幫助,需要的可以參考一下2022-12-12Android使用自定義View實(shí)現(xiàn)橫行時(shí)間軸效果
這篇文章主要給大家介紹了關(guān)于Android使用自定義View實(shí)現(xiàn)橫行時(shí)間軸效果的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Android具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12android實(shí)現(xiàn)添加耳機(jī)狀態(tài)圖標(biāo)的方法
這篇文章主要介紹了android實(shí)現(xiàn)添加耳機(jī)狀態(tài)圖標(biāo)的方法,較為詳細(xì)的分析了Android實(shí)現(xiàn)添加耳機(jī)圖標(biāo)的原理與相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10Android自定義帶進(jìn)度條WebView仿微信加載過(guò)程
這篇文章主要為大家詳細(xì)介紹了Android自定義帶進(jìn)度條WebView仿微信加載過(guò)程,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03Android Application級(jí)別自定義Toast
這篇文章主要為大家詳細(xì)介紹了Android Application級(jí)別自定義Toast,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08android使用surfaceview+MediaPlayer播放視頻
這篇文章主要為大家詳細(xì)介紹了android使用surfaceview+MediaPlayer播放視頻,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11Android中HttpURLConnection與HttpClient的使用與封裝
這篇文章主要介紹了Android中HttpURLConnection與HttpClient的使用以及封裝方法,感興趣的小伙伴們可以參考一下2016-03-03Android 異步獲取網(wǎng)絡(luò)圖片并處理導(dǎo)致內(nèi)存溢出問(wèn)題解決方法
Android異步獲取網(wǎng)絡(luò)圖片并處理圖片Out Of Memory內(nèi)存溢出如何解決呢?本文介紹了操作步驟,感興趣的朋友可以了解下或許對(duì)你有所幫助2013-02-02Android RecyclerView的焦點(diǎn)記憶封裝
這篇文章主要介紹了Android RecyclerView的焦點(diǎn)記憶封裝,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04