CoordinatorLayout 與 Behavior 的一己之見

前言

許多文章都是將CoordinatorLayoutAppbarLayoutCollapsingToolbarLayoutToolbar等放在一塊兒介紹,容易誤解爲這幾個佈局必定要互相搭配,且僅僅適用於這些場景中。html

其實否則,其中最重要的是CoordinatorLayout,我把它稱爲協調佈局。協調什麼佈局呢?天然是嵌套在其內部的 Child View。java

CoordinatorLayout充當了一箇中間層的角色,一邊接收其餘組件的事件,一邊將接收到的事件通知給內部的其餘組件。android

Behavior就是CoordinatorLayout傳遞事件的媒介,Behavior 定義了 CoordinatorLayout直接子 View的行爲規範,決定了當收到不一樣事件時,應該作怎樣的處理。segmentfault

總結來講,Behavior代理如下四種事件,其大體傳遞流程以下圖:數組

事件流好像很高深莫測的樣子...,再簡化一點的說法:CoordinatorLayout中的某個或某幾個方法被其餘類調用,以後CoordinatorLayout再調用Behavior中的某個或某幾個方法(=。=好像更抽象了)。總之,讓這四類事件如今腦子裏有個印象就能夠了。app

接着先介紹一下自定義Behavior的通用流程。爲何是通用流程呢?由於上面提到了有四種事件流,根據不一樣的事件流,是要重寫不一樣的方法的,會在下面一一說明。ide

自定義Behavior的通用流程

1. 重寫構造方法函數

public class CustomBehavior extends CoordinatorLayout.Behavior {

    public CustomBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

必定要重寫這個構造方法,由於當你在XML中設置該Behavior時,在 CoordinatorLayout 中會反射調用該方法,並生成該 Behavior 實例。佈局

2. 綁定到Viewthis

綁定的方法有三種:

在 XML 文件中,設置任意 View 的屬性

app:layout_behavior="你的Behavior的包路徑和類名"

或者在代碼中:

(CoordinatorLayout.LayoutParams)child.getLayoutParams().setBehavior();

再或者當你的View是自定義的View時。在你的自定義View類上添加@DefaultBehavior(你的Behavior.class)。

@DefaultBehavior(CustomBehavior.class)
public class CustomView extends View {}

3. 判斷依賴對象

CoordinatorLayout 收到某個 view 的變化或者嵌套滑動事件時,CoordinatorLayout就會嘗試把事件下發給Behavior,綁定了該 Behavior 的 view 就會對事件作出響應。

下面是這兩個具備依賴的關係的view在Behavior方法中的形參名,方便讀者分辨:被動變化,也就是綁定了Behavior的view稱爲child主動變化的view在「變化事件」中稱爲dependency;在「嵌套滑動事件」中稱爲target

由於可能會存在不少的Child View能夠向CoordinatorLayout發出消息,也同時存在不少的Child View擁有着不一樣的Behavior,那麼在CoordinatorLayout將真正的事件傳遞進這個Behavior以前,確定須要一個方法,告知CoordinatorLayout這二者的依賴關係是否成立。若是關係成立,那麼就把事件下發給你,若是關係不成立,那咱就到此over。

下面以「變化事件」的layoutDependsOn說幾個例子,「嵌套滑動事件」就在onStartNestedScroll中作一樣的判斷。另外的兩種「佈局事件」「觸摸事件」就沒有這一步了。

a.根據id

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency.getId() == R.id.xxx;
}

b.根據類型

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof CustomView;
}

c.根據id的另外一種寫法

<declare-styleable name="Follow">
    <attr name="target" format="reference"/>
</declare-styleable>

先自定義target這個屬性。

<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 最主要的方法就是兩個:

public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent ev)

CoordinatorLayoutonInterceptTouchEventonTouchEvent 方法中,會嘗試調用其 Child View 擁有的 Behavior 中的同名方法。

public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev)
public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev)

若是 Behavior 對觸摸事件進行了攔截,就不會再分發到 Child View 自身擁有的觸摸事件中。這就意味着:在不知道具體View的狀況下,就能夠重寫它的觸摸事件。

然而有一點咱們須要注意到的是:onTouch事件是CoordinatorLayout分發下來的,因此這裏的onTouchEvent並非咱們控件本身的onTouch事件,也就是說,你假如手指不在咱們的控件上滑動,也會觸發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;
}

對手勢的位置進行過濾,不是咱們控件範圍內的,捨棄掉。

2. 佈局事件

視圖佈局無非就是這兩個方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected void onLayout(boolean changed, int l, int t, int r, int b)

CoordinatorLayoutonMeasure onLayout 方法中,也會嘗試調用其 Child View 擁有的 Behavior 中對應的方法,分別是:

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 會優先處理 Behavior 中所重寫的佈局事件。

3. 變化事件

這個變化是指 View 的位置、尺寸發生了變化。在 CoordinatorLayoutonDraw 方法中,會遍歷所有的 Child View 嘗試尋找是否有相互關聯的對象。

肯定是否關聯的方式有兩種:

1. Behavior中定義

經過 BehaviorlayoutDependsOn 方法來判斷是否有依賴關係,若是有就繼續調用 onDependentViewChanged。FloatActionButton 能夠在 Snackbar 彈出時主動上移就經過該方式實現。

/**
 * 判斷是dependency是不是當前behavior須要的對象
 * @param parent CoordinatorLayout
 * @param child 該Behavior對應的那個View
 * @param dependency dependency 要檢查的View(child是否要依賴這個dependency)
 * @return true 依賴, false 不依賴
 */
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
    return false;
}

/**
 * 當改變dependency的尺寸或者位置時被調用
 * @param parent CoordinatorLayout
 * @param child  該Behavior對應的那個View
 * @param dependency child依賴dependency
 * @return true 處理了, false 沒處理
 */
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
    return false;
}

/**
 * 在layoutDependsOn返回true的基礎上以後,通知dependency被移除了
 * @param parent CoordinatorLayout
 * @param child 該Behavior對應的那個View
 * @param dependency child依賴dependency
 */
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, Button child, View dependency) {
    
}

2. XML中設置屬性

經過 XML 中設置的 layout_anchor,關聯設置了 layout_anchor 的 Child View 與 layout_anchor對應的目標 dependency View。隨後調用 offsetChildToAnchor(child, layoutDirection);,其實就是調整二者的位置,讓它們能夠一塊兒變化。FloatActionButton 能夠跟隨 Toolbar 上下移動就是該方式實現。

app:layout_anchor="@id/dependencyView.id"

4. 嵌套滑動事件

實現NestedScrollingChild

若是一個View想向外界傳遞滑動事件,即通知 NestedScrollingParent,就必須實現此接口。

而 Child 與 Parent 的具體交互邏輯,NestedScrollingChildHelper 輔助類基本已經幫咱們封裝好了,因此咱們只須要調用對應的方法便可。

NestedScrollingChild接口的通常實現:

public class CustomNestedScrollingChildView extends View implements NestedScrollingChild {

    private NestedScrollingChildHelper mChildHelper = new NestedScrollingChildHelper(this);

    /**
     * 設置當前View可否滑動
     * @param enabled
     */
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    /**
     * 判斷當前View可否滑動
     * @return
     */
    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    /**
     * 啓動嵌套滑動事件流
     * 1. 尋找能夠接收 NestedScroll 事件的 parent view,即實現了 NestedScrollingParent 接口的 ViewGroup
     * 2. 通知該 parent view,如今我要把滑動的參數傳遞給你
     * @param axes
     * @return
     */
    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

    /**
     * 中止嵌套滑動事件流
     */
    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }

    /**
     * 是否存在接收 NestedScroll 事件的 parent view
     * @return
     */
    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }

    /**
     * 在滑動以後,向父view彙報滾動狀況,包括child view消費的部分和child view沒有消費的部分。
     * @param dxConsumed x方向已消費的滑動距離
     * @param dyConsumed y方向已消費的滑動距離
     * @param dxUnconsumed x方向未消費的滑動距離
     * @param dyUnconsumed y方向未消費的滑動距離
     * @param offsetInWindow 若是parent view滑動致使child view的窗口發生了變化(child View的位置發生了變化)
     *                       該參數返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化
     *                       若是你記錄了手指最後的位置,須要根據參數offsetInWindow計算偏移量,
     *                       才能保證下一次的touch事件的計算是正確的。
     * @return 若是parent view接受了它的滾動參數,進行了部分消費,則這個函數返回true,不然爲false。
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow);
    }

    /**
     * 在滑動以前,先問一下 parent view 是否須要滑動,
     * 即child view的onInterceptTouchEvent或onTouchEvent方法中調用。
     * 1. 若是parent view滑動了必定距離,你須要從新計算一下parent view滑動後剩下給你的滑動距離剩餘量,
     *      而後本身進行剩餘的滑動。
     * 2. 該方法的第三第四個參數返回parent view消費掉的滑動距離和child view的窗口偏移量,
     *      若是你記錄了手指最後的位置,須要根據第四個參數offsetInWindow計算偏移量,
     *      才能保證下一次的touch事件的計算是正確的。
     * @param dx x方向的滑動距離
     * @param dy y方向的滑動距離
     * @param consumed 若是不是null, 則告訴child view如今parent view滑動的狀況,
     *                 consumed[0]parent view告訴child view水平方向滑動的距離(dx)
     *                 consumed[1]parent view告訴child view垂直方向滑動的距離(dy)
     * @param offsetInWindow 可選 length=2 的數組,
     *                       若是parent view滑動致使child View的窗口發生了變化(子View的位置發生了變化)
     *                       該參數返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化
     *                       若是你記錄了手指最後的位置,須要根據參數offsetInWindow計算偏移量,
     *                       才能保證下一次的touch事件的計算是正確的。
     * @return 若是parent view對滑動距離進行了部分消費,則這個函數返回true,不然爲false。
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    /**
     * 在嵌套滑動的child view快速滑動以後再調用該函數向parent view彙報快速滑動狀況。
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @param consumed true 表示child view快速滑動了, false 表示child view沒有快速滑動
     * @return true 表示parent view快速滑動了, false 表示parent view沒有快速滑動
     */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    /**
     * 在嵌套滑動的child view快速滑動以前告訴parent view快速滑動的狀況。
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @return true 表示parent view快速滑動了, false 表示parent view沒有快速滑動
     */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
}

實現NestedScrollingParent

若是一個View Group想接收來自 NestedScrollingChild 的滑動事件,就須要實現該接口。

一樣有一個NestedScrollingParentHelper 輔助類,幫咱們封裝好了 parent view 與 child view之間的具體交互邏輯。

由 NestedScrollingChild 主動發出滑動事件傳遞給 NestedScrollingParent,NestedScrollingParent 作出響應。
之間的調用關係以下表所示:

Child View Parent View
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll
dispatchNestedFling onNestedFling
dispatchNestedPreFling onNestedPreFling

繼承Behavior

在上面的說明中提到 Parent View 會消費一部分或所有的滑動距離,但其實大部分狀況下,咱們的 Parent View 自身並不會消費滑動距離,都是傳遞給 Behavior,也就是擁有這個 Behavior 的 Child View 纔是真正消費滑動距離的實例。

Behavior 擁有與 NestedScrollingParent? 接口徹底同名的方法。在每個 NestedScrollingParent? 的方法中都會調用 Behavior 中的同名方法。

有這麼幾個方法作下特別說明:

/**
 * 開始嵌套滑動的時候被調用
 * 1. 須要判斷滑動的方向是不是咱們須要的。
 *      nestedScrollAxes == ViewCompat.SCROLL_AXIS_HORIZONTAL 表示是水平方向的滑動
 *      nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL 表示是豎直方向的滑動
 * 2. 返回 true 表示繼續接收後續的滑動事件,返回 false 表示再也不接收後續滑動事件
 */
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                                   View directTargetChild, View target, int nestedScrollAxes) {
}

/**
 * 滑動中調用
 * 1. 正在上滑:dyConsumed > 0 && dyUnconsumed == 0
 * 2. 已經到頂部了還在上滑:dyConsumed == 0 && dyUnconsumed > 0
 * 3. 正在下滑:dyConsumed < 0 && dyUnconsumed == 0
 * 4. 已經打底部了還在下滑:dyConsumed == 0 && dyUnconsumed < 0
 */
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,
                           int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}

/**
 * 快速滑動中調用
 */
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                             float velocityX, float velocityY, boolean consumed) {
}

總結

總結一下這四種事件流,和各自須要實現的方法。

根據在自定義Behavior時是否須要判斷依賴關係,把Behavior代理的四種狀況分紅兩類:

事件來自外部父view:

  1. 佈局事件:BehavioronMeasureChild+onLayoutChild

  2. 觸摸事件:BehavioronInterceptTouchEvent+onTouchEvent

事件來自內部子view:

  1. view變化事件:BehaviorlayoutDependsOn+onDependentViewChanged+onDependentViewRemoved

  2. 嵌套滑動事件:BehavioronStartNestedScroll+onNestedScrollAccepted+onStopNestedScroll+onNestedScroll+onNestedPreScroll+onNestedFling+onNestedPreFling

後記

以前在Google、百度自定義Behavior造輪子的時候,剛開始看一篇,以爲不過如此,就這麼點東西。再看一篇,咦~實現怎麼又不同了,再來一篇又不同了。

本文就是想起一個大綱的做用,輪子再怎麼造,仍是這麼些個方法。之後再看別人的輪子或者本身造輪子的時候,能夠清晰一些。

擴展

sidhu眼中的CoordinatorLayout.Behavior(一)
sidhu眼中的CoordinatorLayout.Behavior(二)
sidhu眼中的CoordinatorLayout.Behavior(三)
Material Design系列,自定義Behavior支持全部View
CoordinatorLayout的使用如此簡單

相關文章
相關標籤/搜索