CoordinatorLayout與Behavior的一己之見(jiàn)

前言

許多文章都是將CoordinatorLayout、AppbarLayout、CollapsingToolbarLayoutToolbar等放在一起介紹,容易誤解為這幾個(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)

CoordinatorLayoutonInterceptTouchEventonTouchEvent 方法中,會(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)

CoordinatorLayoutonMeasureonLayout 方法中,也會(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ā)生了變化。
CoordinatorLayoutonDraw 方法中,會(huì)遍歷全部的 Child View 嘗試尋找是否有相互關(guān)聯(lián)的對(duì)象。
確定是否關(guān)聯(lián)的方式有兩種:
1. Behavior中定義
通過(guò) BehaviorlayoutDependsOn 方法來(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.布局事件:BehavioronMeasureChild+onLayoutChild
2.觸摸事件:BehavioronInterceptTouchEvent+onTouchEvent
事件來(lái)自?xún)?nèi)部子view:
3.view變化事件:BehaviorlayoutDependsOn+onDependentViewChanged+onDependentViewRemoved
4.嵌套滑動(dòng)事件:BehavioronStartNestedScroll+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)單

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容