針對 CoordinatorLayout 及 Behavior 的一次細節較真

我認真不是爲了輸贏,我就是認真。– 羅永浩html

我一直對 Material Design 很感興趣,每次在官網上閱讀它的相關文檔時,我總會有更進一步的體會。固然,Material Design 並非僅僅針對 Android 而言的,它實際上是一套廣泛性的設計規範。而對於 Android 開發人員而言,咱們涉及的每每是它的實現。也就是一個個個性鮮明的類。好比 RecyclerView 、CardView、Palette 等等。而且爲了讓開發者更輕鬆地開發出符合 Material Design 設計規範的界面,Google 開發人員直接提供了一個兼容包,它就是 Android Support Design Library。java

引用這個包須要在 build.gradle 中添加依賴。android

compile 'com.android.support:design:25.0.1'複製代碼

在這個包中,最核心的一個類就是 CoordinatorLayout。由於其它的類都須要與它進行相關聯才能互動。而今天的主題就是討論這個類的一些細節。
這裏寫圖片描述git

上圖中這種高大上的視覺和交互效果,第一次看的時候我心頭就癢癢的,巴不得立馬就去實現它。而後,就百度查找相關的博文,可是風格我都不是很喜歡。我不喜歡文章中放一個 xml 佈局文件,而後配置一些屬性,而後就沒有了。github

<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context="com.frank.supportdesigndemo.ScrollingActivity">

    <android.support.design.widget.AppBarLayout  android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:layout_marginTop="-28dp" android:fitsSystemWindows="true" android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout  android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView  android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" android:src="@drawable/test" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.7" />
            <android.support.v7.widget.Toolbar  android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/AppTheme.PopupOverlay" />

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_scrolling" />

    <android.support.design.widget.FloatingActionButton  android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/fab_margin" app:layout_anchor="@id/app_bar" app:layout_anchorGravity="bottom|end" app:srcCompat="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>
複製代碼

我照着完成了,效果也達到了,可是感受有些虛。我不得勁,或者說我心裏糾結吧。心裏千頭萬緒,上面的佈局文件中,除了 Toolbar 我認識外,其它的控件,我一個都不熟悉。編程

我不喜歡這種感受。由於我有許許多多的疑惑。api

CoordinatorLayout 是什麼?
CoordinatorLayout 有什麼做用?
AppBarLayout 是什麼?
AppBarLayout 有什麼做用?
……數組

接下來的文章篇幅會比較長,你們仔細閱讀就好。若是時間不夠,能夠直接拖動到文章最後總結的那一節。不明白的地方再到文章中間部分閱讀相關內容就能夠了。但我但願讀者仍是順序方式閱讀,由於我相信若是你有許多疑惑,個人學習過程也許能夠給你一些提示或者啓迪。markdown

更多的真相

在編程領域,學習一個陌生的事物,最好的途徑可能就是閱讀它的官方文檔或者是源代碼。帶着心中的困惑,我前往 Android 官網,直接挑最顯眼的 CoordinatorLayout 來進行研究。因此這篇文章我主講 CoordinatorLayout。
這裏寫圖片描述app

官網解釋 CoordinatorLayout 是一個超級 FrameLayout,而後能夠做爲一個容器指定與 child 的一些交互規則。

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {}
複製代碼

這是 CoordinatorLayout 的聲明。它本質上就是一個 ViewGroup,注意的是它並無繼承自 FrameLayout,而後實現了 NestedScrollingParent 接口,咱們先無論這個 NestedScrollingParent,NestedScrolliingParent 在文章後面適當的地方我會給出解釋。

官網又說經過給CoordinaotrLayout 中的 child 指定 Behavior,就能夠和 child 進行交互,或者是 child 之間互相進行相關的交互。而且自定義 View 時,能夠經過 DefaultBehavior 這個註解來指定它關聯的 Behavior。這裏出現了新的名詞:Behavior。因而,中斷對 CoordinatorLayout 的跟蹤,轉到 Behavior 細節上來。
這裏寫圖片描述
Behavior 實際上是 CoordinatorLayout 中的一個靜態內部類,而且是個泛型,接受任何 View 類型。官方文檔真是惜字如金,更多的細節須要去閱讀代碼,也就是要靠猜想。這點很不爽的。好吧,官方文檔說 Behavior 是針對 CoordinatorLayout 中 child 的交互插件。記住這個詞:插件。插件也就表明若是一個 child 須要某種交互,它就須要加載對應的 Behavior,不然它就是不具有這種交互能力的。而 Behavior 自己是一個抽象類,它的實現類都是爲了可以讓用戶做用在一個 View 上進行拖拽、滑動、快速滑動等手勢。若是本身要定製某個交互動做,就須要本身實現一個 Behavior。

可是,對於咱們而言,咱們要實現一個 Behavior,咱們用來幹嗎呢?

是的,問問本身吧,咱們若是自定義一個 Behavior,咱們想幹嗎?

前面內容有講過,CoordinatorLayout 能夠定義與它 child 的交互或者是某些 child 之間的交互。

咱們先看看 Behavior 的代碼細節,代碼有精簡。

public static abstract class Behavior<V extends View> {

    public Behavior() { }

    public Behavior(Context context, AttributeSet attrs) {}


    public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }

    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {  return false; }

    public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}


    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
            V child, View directTargetChild, View target, int nestedScrollAxes) {
        return false;
    }

    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
            View directTargetChild, View target, int nestedScrollAxes) {
        // Do nothing
    }

    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
        // Do nothing
    }

    public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
            int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        // Do nothing
    }

    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
            int dx, int dy, int[] consumed) {
        // Do nothing
    }

    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
            float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
            float velocityX, float velocityY) {
        return false;
    }
}複製代碼

通常咱們自定義一個 Behavior,目的有兩個,一個是根據某些依賴的 View 的位置進行相應的操做。另一個就是響應 CoordinatorLayout 中某些組件的滑動事件。 咱們先看第一種狀況。

兩個 View 之間的依賴關係

若是一個 View 依賴於另一個 View。那麼它可能須要操做下面 3 個 API:

public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }

public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {  return false; }

public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}
複製代碼

肯定一個 View 對另一個 View 是否依賴的時候,是經過 layoutDependsOn() 這個方法。注意參數,child 是要判斷的主角,而 dependency 是賓角,若是 return true,表示依賴成立,反之不成立。固然,你能夠複寫這個方法對 dependency 進行類型判斷不然是其它條件判斷,而後再決定是否依賴。只有在 layoutDependsOn() 返回爲 true 時,後面的 onDependentViewChanged() 和 onDependentViewRemoved() 纔會被調用。

當依賴的那個 View 發生變化時,這個變化代碼註釋有解釋,指的是 dependency 的尺寸和位置發生的變化,當有變化時 Behavior 的 onDependentViewChanged() 方法會被調用。若是複寫這個方法時,改變了 child 的尺寸和位置參數,則須要返回 true,默認狀況是返回 false。

onDependentView() 被調用時通常是指 dependency 被它的 parent 移除,或者是 child 設定了新的 anchor。

有了上面 3 個 API,咱們就能應付在 CoordinatorLayout 中一個子 View 對別個一個子 View 的依賴情景了。

可能會有同窗不明白,依賴是爲什麼?或者說是何種依賴。爲了不概念過於空洞抽象。下面,咱們用一個簡單的例子來讓你們感覺一下,加深理解。

爲了演示效果,我首先在屏幕上定義一個可以響應拖動的自定義 View,我叫它 DependencyView 好了。
這裏寫圖片描述
它的代碼很簡單,主要是繼承一個 TextView,而後在觸摸事件中對自身位置進行位移。

public class DependencyView extends TextView {

    private final int mSlop;
    private float mLastX;
    private float mLastY;

    public DependencyView(Context context) {
        this(context,null);
    }

    public DependencyView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public DependencyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setClickable(true);

        mSlop = ViewConfiguration.getTouchSlop();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
       // return super.onTouchEvent(event);
        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                int deltax = (int) (event.getX() - mLastX);
                int deltay = (int) (event.getY() - mLastY);
                if (Math.abs(deltax) > mSlop || Math.abs(deltay) > mSlop) {
                    ViewCompat.offsetTopAndBottom(this,deltay);
                    ViewCompat.offsetLeftAndRight(this,deltax);
                    mLastX = event.getX();
                    mLastY = event.getY();
                }

                break;

            case MotionEvent.ACTION_UP:
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            default:
                break;

        }

        return true;
    }
}
複製代碼

前置條件已經肯定好了,如今咱們要向目標 Behavior 出發了。

作一個跟屁蟲

實現一個 Behavior,讓它支配一個 View 去牢牢跟隨所依賴的 View。在這裏,咱們讓依賴方始終顯示在被依賴方的正下方,不論被依賴方位置怎麼變換,依賴方始終牢牢相隨。那麼,代碼怎麼寫。

public class MyBehavior extends CoordinatorLayout.Behavior <View>{

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

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        int dependBottom = dependency.getBottom();

        child.setY(dependBottom + 50);
        child.setX(dependency.getLeft());

        return true;
    }

}複製代碼

咱們省略了其它代碼,只保留了核心的兩個方法,你們一看就懂。經過判斷 dependency 是否爲 DependencyView 類型來決定是否對其進行依賴。而後在 onDependentViewChanged() 方法中獲取 dependency 的位置參數來設置 child 的位置參數,從而實現了預期效果。注意的是更改 child 的位置後,要 return true。

下面來驗證。咱們在佈局文件中對一個 ImageView 設置 MyBehavior,而後觀察它的現象。

<ImageView  android:id="@+id/iv_test" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>複製代碼

這裏寫圖片描述

固然這種依賴,並不是是一對一的關係,多是一對多。或者是多對多。

咱們再修改一個代碼,若是 child 是一個 TextView 就讓它始終在 dependency 的上方顯示,不然在它下方顯示。

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

    float x = child.getX();
    float y = child.getY();

    int dependTop= dependency.getTop();
    int dependBottom = dependency.getBottom();

    x = dependency.getX();

    if ( child instanceof TextView ) {
        y = dependTop - child.getHeight() - 20;
    } else {
        y = dependBottom + 50;
    }


    child.setX(x);
    child.setY(y);

    return true;
}
複製代碼

上面代碼清晰易懂,咱們再在 xml 佈局文件中添加一個 TextView。

<ImageView  android:id="@+id/iv_test" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>
<TextView  android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="for test" app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>複製代碼

效果以下:
這裏寫圖片描述

到此,咱們算是弄明白了在 Behavior 中針對被依賴的對象尺寸及位置變化時,依賴方應該如何處理的流程了。接着往下的內容就是處理滑動相關了。不過,在這以前先對一個地方進行說明,那就是如何對於一個 View 設置 Behavior。

Behavior 的設置方法

1. 在 xml 屬性中進行設置

對應屬性是 app:layout_behavior。要設置的是一條字符串,通常是 Behavior 的全限定類名如 com.frank.supportdesigndemo.MyBehavior,固然,在當前目錄下你能夠用 . 代替如 .MyBehavior

2. 在代碼中設置

主要是設置對應 View 的 LayoutParam

CoordinatorLayout.LayoutParams layoutParams = 
    (CoordinatorLayout.LayoutParams) mIvTest.getLayoutParams();

layoutParams.setBehavior(new MyBehavior());
複製代碼

3. 經過註解

自定義 View 時,經過 CoordinatorLayout.DefaultBehavior 這個註解,就能夠爲該 View 默認綁定一個對應的 Behavior。Android 源碼中有現成的例子。

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {}複製代碼

能夠看到 AppBarLayout 被註解綁定了 AppBarLayout.Behavior 這個 Behavior。因此,以後要研究 AppBarLayout 的話也須要研究它的 Behavior。不過,這是後話。

Behavior 對滑動事件的響應。

其實對於這樣的行爲,我存在過困惑。官方文檔的內容太少了,說的是滑動,可是我並不明白是什麼滑動。是響應誰的滑動。

咱們通常接觸到的滑動控件是 ScrollView、ListView 和 RecyclerView。而 CoordinatorLayout 自己可以滑動嗎?

滑動相關的代碼以下:

public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
            V child, View directTargetChild, View target, int nestedScrollAxes) {
        return false;
    }

public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
        View directTargetChild, View target, int nestedScrollAxes) {
    // Do nothing
}

public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
    // Do nothing
}

public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    // Do nothing
}

public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dx, int dy, int[] consumed) {
    // Do nothing
}

public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY, boolean consumed) {
    return false;
}

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY) {
    return false;
}
複製代碼

爲了觀察滑動這個行爲,我在 MyBehavior 中進行編寫了一些調試代碼。

@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,
                           int dxConsumed, int dyConsumed, int dxUnconsumed,
                           int dyUnconsumed) {
    Log.d(TAG,"onNestedScroll:"+dxConsumed+" dy:"+dyConsumed);
    super.onNestedScroll(coordinatorLayout, child, target, dxConsumed,
            dyConsumed, dxUnconsumed, dyUnconsumed);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                                   View directTargetChild, View target, int nestedScrollAxes) {
    Log.d(TAG,"onStartNestedScroll");
    return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
            nestedScrollAxes);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    Log.d(TAG,"onNestedPreScroll dx:"+dx+" dy:"+dy);
}

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                             float velocityX, float velocityY, boolean consumed) {
    Log.d(TAG,"onNestedFling");
    return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}複製代碼

而後我就在模擬器上用鼠標在 CoordinatorLayout 上拼命滑動,想製造一些滑動事件出來,看看 MyBehavior 相應的 API 能不能觸發,而後觀察 Log。
這裏寫圖片描述

很遺憾,我無功而返。

認真查閱文檔和源碼。我將注意力放在 onStartNestedScroll() 方法上了。

/** * Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll. * * <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond * to this event and return true to indicate that the CoordinatorLayout should act as * a nested scrolling parent for this scroll. Only Behaviors that return true from * this method will receive subsequent nested scroll events.</p> * * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is * associated with * @param child the child view of the CoordinatorLayout this Behavior is associated with * @param directTargetChild the child view of the CoordinatorLayout that either is or * contains the target of the nested scroll operation * @param target the descendant view of the CoordinatorLayout initiating the nested scroll * @param nestedScrollAxes the axes that this nested scroll applies to. See * {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, * {@link ViewCompat#SCROLL_AXIS_VERTICAL} * @return true if the Behavior wishes to accept this nested scroll * * @see NestedScrollingParent#onStartNestedScroll(View, View, int) */
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
        V child, View directTargetChild, View target, int nestedScrollAxes) {
    return false;
}
複製代碼

註釋說了,當一個 CoordinatorLayout 的後代企圖觸發一個 nested scroll 事件時,這個方法被調用。nested scroll 我不知道是什麼,有些人稱呼爲嵌套滑動。那就用嵌套滑動來翻譯吧。註釋中說過,只有在 onStartNestedSroll() 方法返回 true 時,後續的嵌套滑動事件纔會響應。

後續的響應函數應該就是指的是這幾個方法

public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
        View directTargetChild, View target, int nestedScrollAxes) {}

public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {}

public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {}

public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dx, int dy, int[] consumed) {}

public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY, boolean consumed) {}

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY) {}複製代碼

那麼,咱們從源頭方法 onStartNestedScroll() 中開始分析。

藉助於 AndroidStudio,咱們很容易查找到 Behavior 中 onStartNestedScroll() 方法在哪裏被調用。
這裏寫圖片描述

原來,它是在 CoordinatorLayout 中被調用,咱們跟進去看一看。

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    boolean handled = false;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                    nestedScrollAxes);
            handled |= accepted;

            lp.acceptNestedScroll(accepted);
        } else {
            lp.acceptNestedScroll(false);
        }
    }
    return handled;
}
複製代碼

這是 CoordinatorLayout 中的一個方法,它獲取子 View 的 Behavior,而後調用 Behavior 的 onStartNestedScroll() 方法。

再深刻一點,誰調用了 CoordinatorLayout 的 onStartNestedScroll() 呢?

咱們繼續追蹤。
這裏寫圖片描述

發現有 3 個類能夠調用它,一個是 View,另一個是 ViewParentCompatLollipop 和 ViewParentCompatStubImpl。那麼其實歸根到底就是 View 和 ViewParentCompat。咱們先從 View 開始分析好了。

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = getParent();
        View child = this;
        while (p != null) {
            try {
                if (p.onStartNestedScroll(child, this, axes)) {
                    mNestedScrollingParent = p;
                    p.onNestedScrollAccepted(child, this, axes);
                    return true;
                }
            } catch (AbstractMethodError e) {
                Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
                        "method onStartNestedScroll", e);
                // Allow the search upward to continue
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}
複製代碼

邏輯已經很清晰了,當一個 View 的 startNestedScroll() 方法觸發時,若是符合規則,則會遍歷本身的 parent,調用 parent 的 onStartNestedScroll() 方法。由於 CoordinatorLayout 是一個 ViewGroup,因此它就是一個 ViewParent 對象。因此,若是一個 CoordinatorLayout 中的後代觸發了 startNestedScroll() 方法,若是符合某種條件,那麼它的 onStartNestedScroll() 方法就會調用,再進一步會調用相應 Behavior 的方法。

注意我在上文中的措辭,我說過符合規則或者說符合某種條件,那麼條件的具體是什麼呢?

/** * Returns true if nested scrolling is enabled for this view. * * <p>If nested scrolling is enabled and this View class implementation supports it, * this view will act as a nested scrolling child view when applicable, forwarding data * about the scroll operation in progress to a compatible and cooperating nested scrolling * parent.</p> * * @return true if nested scrolling is enabled * * @see #setNestedScrollingEnabled(boolean) */
public boolean isNestedScrollingEnabled() {
    return (mPrivateFlags3 & PFLAG3_NESTED_SCROLLING_ENABLED) ==
            PFLAG3_NESTED_SCROLLING_ENABLED;
}
複製代碼

當 isNestedScrollingEnabled() 返回 true 時,它的 ViewParent 的 onStartNestedScroll() 才能被觸發。這個方法的邏輯就是判斷一個 View 中 mPrivateFlags3 這個變量中的 PFLAG3_NESTED_SCROLLING_ENABLED 這一 bit 是否被置爲 1 。

註釋有提到,另一個方法 setNestedScrollingEnabled() 來設置能不能擁有嵌套滑動的能力。

public void setNestedScrollingEnabled(boolean enabled) {
    if (enabled) {
        mPrivateFlags3 |= PFLAG3_NESTED_SCROLLING_ENABLED;
    } else {
        stopNestedScroll();
        mPrivateFlags3 &= ~PFLAG3_NESTED_SCROLLING_ENABLED;
    }
}複製代碼

看到這裏的時候,我有了一個大膽的想法。

大膽,用 Button 產生一個 nested scroll 事件

若是一個 View 符合嵌套滑動的條件。也就是經過調用 setNestedScrollingEnabled(true),而後調用它的 startNestedScroll() 方法,它理論上是應該能夠產生嵌套滑動事件的。好吧,咱們來試一下,咱們在佈局文件中添加一個普通的 Button,而後給它設置點擊事件。代碼以下:

mBtnTest = (Button) findViewById(R.id.btn_nested_scroll);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    mBtnTest.setNestedScrollingEnabled(true);
}

mBtnTest.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mBtnTest.startNestedScroll(View.SCROLL_AXIS_HORIZONTAL);
        }
    }
});
複製代碼

由於我以前在 MyBehavior 中對相關的方法有 log 代碼,因此若是 CoordinatorLayout 中發生嵌套滑動事件,log 是有輸出的。

這裏寫圖片描述

如上圖所示,結果符合預期。不過,咱們看上面的代碼,當一個 View 只有在版本在 Lollipop 及以上時,它才能調用嵌套滑動相關的 api。若是是 5.0 版本如下呢?其實系統作了兼容。

mBtnTest = (Button) findViewById(R.id.btn_nested_scroll);
ViewCompat.setNestedScrollingEnabled(mBtnTest,true);
mBtnTest.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            ViewCompat.startNestedScroll(mBtnTest,View.SCROLL_AXIS_HORIZONTAL);
        }
    }
});
複製代碼

咱們能夠經過 ViewCompat 這個類來完成對應的操做。不停歇,繼續跟蹤下去。

public static void setNestedScrollingEnabled(View view, boolean enabled) {
        IMPL.setNestedScrollingEnabled(view, enabled);
}
複製代碼

經過 IMPL 這個代理來完成。

static final ViewCompatImpl IMPL;
static {
    final int version = android.os.Build.VERSION.SDK_INT;
    if (BuildCompat.isAtLeastN()) {
        IMPL = new Api24ViewCompatImpl();
    } else if (version >= 23) {
        IMPL = new MarshmallowViewCompatImpl();
    } else if (version >= 21) {
        IMPL = new LollipopViewCompatImpl();
    } else if (version >= 19) {
        IMPL = new KitKatViewCompatImpl();
    } else if (version >= 18) {
        IMPL = new JbMr2ViewCompatImpl();
    } else if (version >= 17) {
        IMPL = new JbMr1ViewCompatImpl();
    } else if (version >= 16) {
        IMPL = new JBViewCompatImpl();
    } else if (version >= 15) {
        IMPL = new ICSMr1ViewCompatImpl();
    } else if (version >= 14) {
        IMPL = new ICSViewCompatImpl();
    } else if (version >= 11) {
        IMPL = new HCViewCompatImpl();
    } else {
        IMPL = new BaseViewCompatImpl();
    }
}
複製代碼

針對不一樣的系統版本,IMPL 有不一樣的實現,因此它纔可以作到兼容。咱們知道在 Lollipop 版本 View 已經自帶了嵌套滑動和相關屬性和方法,如今咱們就關心最低的版本,它們是如何處理這種狀況的。最低的兼容版本是 BaseViewCompatImpl。

static class BaseViewCompatImpl implements ViewCompatImpl {

    @Override
    public void setNestedScrollingEnabled(View view, boolean enabled) {
        if (view instanceof NestedScrollingChild) {
            ((NestedScrollingChild) view).setNestedScrollingEnabled(enabled);
        }
    }

    @Override
    public boolean isNestedScrollingEnabled(View view) {
        if (view instanceof NestedScrollingChild) {
            return ((NestedScrollingChild) view).isNestedScrollingEnabled();
        }
        return false;
    }



    @Override
    public boolean startNestedScroll(View view, int axes) {
        if (view instanceof NestedScrollingChild) {
            return ((NestedScrollingChild) view).startNestedScroll(axes);
        }
        return false;
    }


}
複製代碼

代碼有刪簡,但足夠水落石出了。若是在 5.0 的系統版本如下,若是一個 View 想發起嵌套滑動事件,你得保證這個 View 實現了 NestedScrollingChild 接口。

想觸發嵌套滑動事件嗎?你是 NestedScrollingChild 嗎?

若是在 5.0 的系統版本以上,咱們要 setNestedScrollingEnabled(true),若是在這個版本如下,得保證這個 View 自己是 NestedScrollingChild 的實現類才行。如今就須要把焦點放在 NestedScrollingChild 上了。

public interface NestedScrollingChild {

    public void setNestedScrollingEnabled(boolean enabled);


    public boolean isNestedScrollingEnabled();


    public boolean startNestedScroll(int axes);


    public void stopNestedScroll();


    public boolean hasNestedScrollingParent();


    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);


    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);


    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);


    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
複製代碼

藉助於 AndroidStudio,經過 CTRL + T 快捷鍵,咱們能夠獲得目前 NestedScrollingChild 的實現者。

這裏寫圖片描述

它有 4 個實現類:NavigationMenuView、NestedScrollView、RecyclerView、SwipleRefreshLayout。

好吧,此次追蹤完結了。咱們不須要將一個 View 設置可以滑動,而後再模擬滑動事件了。如今系統提供了 4 個來挑選。RecyclerView 和 SwipleRefreshLayout 咱們天然是熟悉,NestedScrollView 看名字就能夠聯想到是能產生嵌套滑動的 ScrollView。

接下來的任務是什麼?

別忘記了這一節的主題是自定義 Behavior。咱們只在第一部分探索了 child 之間的依賴互動關係,尚未去討論 Behavior 中如何響應嵌套滑動事件。以前的千迴百轉,我只是想找到可以挑起嵌套滑動事端的 View 而已。如今找到了以後,咱們繼續以前的話題。咱們如今將一個 NestedScrollView 放進佈局文件中,滑動它的內容,它將產生嵌套滑動事件。Behavior 須要針對自身業務邏輯進行相應的處理。

<android.support.v4.widget.NestedScrollView  android:layout_marginTop="200dp" android:layout_width="300dp" android:layout_height="wrap_content">
    <TextView  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:text="@string/large_text" />
</android.support.v4.widget.NestedScrollView>
複製代碼

咱們的目的是當 NestedScrollView 內容滑動時,MyBehavior 規定關聯的 ImageView 對象進行相應的位移,這主要是在 Y 軸方向上。首先咱們得實現這個方法。

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                                   View directTargetChild, View target, int nestedScrollAxes) {
    Log.d(TAG,"onStartNestedScroll");
    return child instanceof ImageView && nestedScrollAxes == View.SCROLL_AXIS_VERTICAL;
}
複製代碼

只有 child 是 ImageView 類型,而且滑動的方向是 Y 軸時才響應。而後,咱們能夠針對滑動事件產生的位移對 child 進行操做了。

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    Log.d(TAG,"onNestedPreScroll dx:"+dx+" dy:"+dy);
    ViewCompat.offsetTopAndBottom(child,dy);
}
複製代碼

咱們要複寫 onNestedPreScroll() 方法,dx 和 dy 是滑動的位移。另外還有一個方法 onNestedScroll()。兩個方法的不一樣在於順序的前後,onNestedPreScroll() 在 滑動事件準備做用的時候先行調用,注意是準備做用,而後把已經消耗過的距離傳遞給 consumed 這個數組當中。而 onNestedScroll() 是滑動事件做用時調用的。它的參數包括位移信息,以及已經在 onNestedPreScroll() 消耗過的位移數值。咱們通常實現 onNestedPreScroll() 方法就行了。

在上面代碼中,咱們經過讀取 dy 的值,來讓 child 進行 Y 軸方向上的移動。 固然,在這以前咱們要將 MyBehavior 作一些處理。將它與 TestView 解除依賴。

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    //return dependency instanceof DependencyView;
    return false;
}複製代碼

而後,咱們回顧下 xml 佈局文件。

<ImageView  android:id="@+id/iv_test" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>
<TextView  android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="for test" app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>

<android.support.v4.widget.NestedScrollView  android:layout_marginTop="200dp" android:layout_width="300dp" android:layout_height="wrap_content">
    <TextView  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:text="@string/large_text" />
</android.support.v4.widget.NestedScrollView>複製代碼

佈局中 ImageView 和 TextView 同時被設置了 MyBehavior 屬性,可是根據代碼邏輯,最終應該只有 ImageView 可以進行 Y 軸方向的移動。那麼事實如何?

這裏寫圖片描述

效果達到了預期。

可能有細心的同窗還會發現,Byhavior 中有兩個與 Fling 相關的API。

public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY, boolean consumed) {
    return false;
}

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY) {
    return false;
}
複製代碼

顧名思義,這就和 fling 操做有關。快速滑動 NestedScrollView 或者 RecyclerView,手指停下來的時候,滑動並無立刻中止,這就是 fling 操做。 與前面的 NestedScroll 類似,咱們能夠在 fling 動做即將發生時,經過 onNestedPreFling 獲知,若是在這個方法返回值爲 true 的話會怎麼樣?它將會攔截此次 fling 動做,代表響應中的 child 本身處理了此次 fling 意圖,那麼 NestedScrollView 反而操做不了這個動做,由於系統會看成 child 消耗過此次事件。你們能夠自行去嘗試一下。咱們把注意點放在一個有趣的實驗上。

這個實驗的目的是當 MyBehavior 響應 fling 動做時,若是滑動方向向下,ImageView 就放大。反之縮小到原先的大小。

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
    Log.d(TAG,"onNestedPreFling velocityY:"+velocityY);
    if ( velocityY > 0 ) {
        child.animate().scaleX(2.0f).scaleY(2.0f).setDuration(2000).start();
    } else {
        child.animate().scaleX(1.0f).scaleY(1.0f).setDuration(2000).start();
    }

    return false;
// return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}複製代碼

在上面的代碼中,我運用了一個 ViewPropertyAnimator 動畫來對 scaleX 和 scaleY 進行處理。咱們來看看效果吧。

這裏寫圖片描述

挺有意思的是嗎?

自定義 Behavior 的總結

  1. 肯定 CoordinatorLayout 中 View 與 View 之間的依賴關係,經過 layoutDependsOn() 方法,返回值爲 true 則依賴,不然不依賴。
  2. 當一個被依賴項 dependency 尺寸或者位置發生變化時,依賴方會經過 Byhavior 獲取到,而後在 onDependentViewChanged 中處理。若是在這個方法中 child 尺寸或者位置發生了變化,則須要 return true。
  3. 當 Behavior 中的 View 準備響應嵌套滑動時,它不須要經過 layoutDependsOn() 來進行依賴綁定。只須要在 onStartNestedScroll() 方法中經過返回值告知 ViewParent,它是否對嵌套滑動感興趣。返回值爲 true 時,後續的滑動事件才能被響應。
  4. 嵌套滑動包括滑動(scroll) 和 快速滑動(fling) 兩種狀況。開發者根據實際狀況運用就行了。
  5. Behavior 經過 3 種方式綁定:1. xml 佈局文件。2. 代碼設置 layoutparam。3. 自定義 View 的註解。

弄清楚上面的規則後,恭喜你,你已經掌握了自定義 Behavior 的基礎技能。

再多一點細節?

不知道你們還記得不,文章前面部分我有試圖去找出誰能產生 Nested Scroll 事件,結果發現它須要是 NestedScrollChild 對象。可是,咱們忽略了一個細節,這個細節就是一個 NestedScrollChild 調用 startNestedScroll() 方法時,其實它須要藉助它的祖先的力量。只有某個祖先的 onStartNestedScroll() 返回爲真的時候,它持續事件才能延續下去。

view.java

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = getParent();
        View child = this;
        while (p != null) {
            try {
                if (p.onStartNestedScroll(child, this, axes)) {
                    mNestedScrollingParent = p;
                    p.onNestedScrollAccepted(child, this, axes);
                    return true;
                }
            } catch (AbstractMethodError e) {
                Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
                        "method onStartNestedScroll", e);
                // Allow the search upward to continue
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}
複製代碼

毫無疑問,ViewParent 充當了重要的角色。

public interface ViewParent {

    public void requestLayout();


    public void invalidateChild(View child, Rect r);


    public ViewParent getParent();


    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);


    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);


    public void onStopNestedScroll(View target);


    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);


    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);


    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);


    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

}
複製代碼

ViewParent 只是一個接口,常見的實現類是 ViewGroup,我曾經一度把 ViewParent 等同於 ViewGroup,結果在分析 View 繪製流程時吃盡苦頭,由於 ViewParent 還有其它的實現類,好比 ViewRoot。不過這是題外話,咱們接着聊 ViewParent。ViewParent 提供了 nested scroll 相關的 API,可是 5.0 版本才加進去的,若是要兼容的話,咱們須要分析 ViewParentCompat 這個類。

public final class ViewParentCompat {

    interface ViewParentCompatImpl {
        public boolean requestSendAccessibilityEvent(
                ViewParent parent, View child, AccessibilityEvent event);
        boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes);
        void onNestedScrollAccepted(ViewParent parent, View child, View target,
                int nestedScrollAxes);
        void onStopNestedScroll(ViewParent parent, View target);
        void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed);
        void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed);
        boolean onNestedFling(ViewParent parent, View target, float velocityX, float velocityY,
                boolean consumed);
        boolean onNestedPreFling(ViewParent parent, View target, float velocityX, float velocityY);
        void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child,
                View source, int changeType);
    }

    static class ViewParentCompatStubImpl implements ViewParentCompatImpl {


        @Override
        public boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes) {
            if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
            return false;
        }
    }
}
複製代碼

此次,又挖掘出了一些真相。在 5.0 版本如下,若是一個 ViewParent 要響應嵌套滑動事件,就得保證它本身是一個 NestedScrollingParent 對象。

public interface NestedScrollingParent {


public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);


public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);


public void onStopNestedScroll(View target);


public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
        int dxUnconsumed, int dyUnconsumed);


public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);


public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);


public boolean onNestedPreFling(View target, float velocityX, float velocityY);


public int getNestedScrollAxes();
}
複製代碼

仍是藉助於 AndroidStudio 和快捷鍵 CTRL+T。咱們能夠獲得它的實現類。
這裏寫圖片描述

咱們能夠發現目前系統已經實現的 NestedScrollingParent 有 4 個:ActionBarOverlayLayout、CoordinatorLayout、NestedScrollView 和 SwipleRefreshLayout。

本文所討論的 CoordinatorLayout 之因此可以處理嵌套滑動事件,這是由於它自己是一個 NestedScrollingParent。另一個有意思的地方就是,NestedScrollView 它有兩種身份,它同時實現了 NestedScrollingChild 和 NestedScrollParent 接口,也就說明它具有了兩種能力,這爲它自己的擴展提供了許多可能性。

Nested scroll 的流程

到這裏的時候,一個嵌套滑動的事件的起始咱們才完全明白。它是由一個 NestedScrollingChild(5.0 版本 setNestedScrollEnable(true) 就行了) 發起,經過向上遍歷 parent,藉助於 parent 對象的相關方法來完成交互。值得注意的是 5.0 版本如下,parent 要保證是一個 NestedScrollingParent 對象。

咱們今天的文章是分析 CoordinatorLayout 及它的 Behavior,因此用一張圖來概念更清晰明瞭。
這裏寫圖片描述

文章到此,又能夠完結了。對於自定義一個 Behavior 而言,咱們已經明白了它的功能及如何實現本身特定的功能。可是對於 CoordinatorLayout 自己而言,它還有許多細節須要說明。可是這些細節跟通用的自定義 ViewGroup 並沒有多大差異。惟一不一樣的地方的由於 Behavior 的存在。

CoordinatorLayout 的其它細節

Behavior 在以前也說過,它是一種插件。正由於這種機制,它將干涉 CoordinatorLayout 與 childView 之間的關係,Behavior 經過攔截 CoordinatorLayout 發給子 View 的信號,根據自身的規則進而來達到控制 childView 的目的。若是沒有這些 Behavior 存在的話,CoordinatorLayout 跟普通的 ViewGroup 無疑。

那麼,Behavior 干涉了 CoordinatorLayout 與它的 childView 之間的什麼?能夠說是方方面面。從測量、佈局到觸摸事件等等。
這裏寫圖片描述

CoordinatorLayout 測量

CoordiantorLayout 是一個超級 FrameLayout,可是它卻並非 FrameLayout 的直接子類,只是一個普通的 ViewGroup 子類而已。FrameLayout 有什麼物質?它就是一層一層的,按照位置信息進行佈局,並無像 LinearLayout 與 RelativeLayout 那麼多約束。

因此,在查看相關代碼以前,咱們能夠猜想的是,CoordinatorLayout 在 wrap_content 這種狀況下,寬高的尺寸信息主要是要找出它子 View 的最大寬度或者最大高度,固然,還得參考 CoordianatorLayout 自己 parent 給它的建議尺寸。那麼,實際狀況是否是這樣子呢?

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();


    final int paddingLeft = getPaddingLeft();
    final int paddingTop = getPaddingTop();
    final int paddingRight = getPaddingRight();
    final int paddingBottom = getPaddingBottom();
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final boolean isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    final int widthPadding = paddingLeft + paddingRight;
    final int heightPadding = paddingTop + paddingBottom;

    int widthUsed = getSuggestedMinimumWidth();
    int heightUsed = getSuggestedMinimumHeight();
    int childState = 0;

    final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);

    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        int childWidthMeasureSpec = widthMeasureSpec;
        int childHeightMeasureSpec = heightMeasureSpec;


        final Behavior b = lp.getBehavior();
        if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0);
        }

        widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                lp.leftMargin + lp.rightMargin);

        heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                lp.topMargin + lp.bottomMargin);
        childState = ViewCompat.combineMeasuredStates(childState,
                ViewCompat.getMeasuredState(child));
    }

    final int width = ViewCompat.resolveSizeAndState(widthUsed, widthMeasureSpec,
            childState & ViewCompat.MEASURED_STATE_MASK);
    final int height = ViewCompat.resolveSizeAndState(heightUsed, heightMeasureSpec,
            childState << ViewCompat.MEASURED_HEIGHT_STATE_SHIFT);
    setMeasuredDimension(width, height);
}
複製代碼

上面的代碼中,我精簡了一些線索無關的代碼。咱們重點要關注 widthUsed 和 heightUsed 兩個變量,它們的做用就是爲了保存 CoordinatorLayout 中最大尺寸的子 View 的尺寸。而且,在對子 View 進行遍歷的時候,CoordinatorLayout 有主動向子 View 的 Behavior 傳遞測量的要求,若是 Behavior 自主測量了 child,則以它的結果爲準,不然將調用 measureChild() 方法親自測量。

CoordinatorLayout 佈局

在 FrameLayout 中佈局默認從左上角開始,可是能夠經過 layoutparam 中的 Gravity 進行佈局對齊。那麼,CoordinatorLayout 佈局時會如何表現?

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);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();

        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}
複製代碼

能夠看到,CoordinatorLayout 將佈局交給了子 View 的 Behavior,讓它自行處理,若是 Behavior 沒有處理關聯的 View 佈局的話,CoordinatorLayout 就會調用 onLayoutChild() 方法佈局。咱們再跟進去。

public void onLayoutChild(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (lp.checkAnchorChanged()) {
        throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
                + " measurement begins before layout is complete.");
    }
    if (lp.mAnchorView != null) {
        layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
    } else if (lp.keyline >= 0) {
        layoutChildWithKeyline(child, lp.keyline, layoutDirection);
    } else {
        layoutChild(child, layoutDirection);
    }
}
複製代碼

這一下引出了三個方法,而且有優先級的。layoutChildWithAnchor 優先級最高,而後是 layoutChildWithKeyline,最後纔是普通的 layoutChild。

CoordinatorLayout 的錨定

LayoutParam 中有個 mAnchorView,Anchor 是錨點的意思,好比 View A 錨定了 View B,那麼 View A 的 mAnchorView 就是 View B,佈局的時候 View A 將參考 View B 的座標。而且 layoutDirection 是參考的方向。它們均可以經過 xml 配置。

app:layout_anchor="@id/btn_coord"
app:layout_anchorGravity="bottom"
複製代碼

須要注意的是,當 View A 錨定 View B 時,就說明 View A 依賴於視圖 View B。這個並不須要在 Behavior 中的 layoutDependsOn 返回 true。具體細節是由於下面的代碼:

boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency == mAnchorDirectChild
            || shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
            || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}
複製代碼

上面這個方法是 LayoutPara 中的方法,判斷 child 是否是依賴於 dependency,先判斷的是 dependency 是否被 child 錨定,若是是的話就無需調用 Behavior 的 layoutDependsOn。

CoordinatorLayout 的參考線

除了錨定這個概念外,出現了 keyline 這個概念。keyline 應該是參考線的意思。指名這個 childView 佈局時根據 keyline 的偏移量,再結合相應的 Gravity 進行佈局。篇幅有限,感興趣的同窗自行去實踐一下相應場景。

最後,咱們彙集普通的 layoutChild() 方法。

private void layoutChild(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect parent = mTempRect1;
    parent.set(getPaddingLeft() + lp.leftMargin,
            getPaddingTop() + lp.topMargin,
            getWidth() - getPaddingRight() - lp.rightMargin,
            getHeight() - getPaddingBottom() - lp.bottomMargin);

    if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this)
            && !ViewCompat.getFitsSystemWindows(child)) {
        // If we're set to handle insets but this child isn't, then it has been measured as
        // if there are no insets. We need to lay it out to match.
        parent.left += mLastInsets.getSystemWindowInsetLeft();
        parent.top += mLastInsets.getSystemWindowInsetTop();
        parent.right -= mLastInsets.getSystemWindowInsetRight();
        parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
    }

    final Rect out = mTempRect2;
    GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
            child.getMeasuredHeight(), parent, out, layoutDirection);
    child.layout(out.left, out.top, out.right, out.bottom);
}
複製代碼

果然如我所推測的同樣,它只是經過 GravityCompat.apply() 方法,經過 Gravity 肯定 childView 在 parent 中的顯示位置。這個效果就等同於了 FrameLayout。

經過測量、佈局以後,CoordinatorLayout 就能夠正常繪製了。可是若是要進行一些觸摸輸入間的交互就還要分析一個內容。這就是它的 touch 相關的事件。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
        // Safe since performIntercept guarantees that
        // mBehaviorTouchView != null if it returns true
        final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
        }
    }

    // Keep the super implementation correct
    if (mBehaviorTouchView == null) {
        handled |= super.onTouchEvent(ev);
    } else if (cancelSuper) {
        if (cancelEvent == null) {
            final long now = SystemClock.uptimeMillis();
            cancelEvent = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
        }
        super.onTouchEvent(cancelEvent);
    }

    if (!handled && action == MotionEvent.ACTION_DOWN) {

    }

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }

    return handled;
}

private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        final List<View> topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);

        // Let topmost child views inspect first
        final int childCount = topmostChildList.size();
        for (int i = 0; i < childCount; i++) {
            final View child = topmostChildList.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior b = lp.getBehavior();

            if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
                // Cancel all behaviors beneath the one that intercepted.
                // If the event is "down" then we don't have anything to cancel yet.
                if (b != null) {
                    if (cancelEvent == null) {
                        final long now = SystemClock.uptimeMillis();
                        cancelEvent = MotionEvent.obtain(now, now,
                                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                    }
                    switch (type) {
                        case TYPE_ON_INTERCEPT:
                            b.onInterceptTouchEvent(this, child, cancelEvent);
                            break;
                        case TYPE_ON_TOUCH:
                            b.onTouchEvent(this, child, cancelEvent);
                            break;
                    }
                }
                continue;
            }

            if (!intercepted && b != null) {
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    case TYPE_ON_TOUCH:
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                if (intercepted) {
                    mBehaviorTouchView = child;
                }
            }

            // Don't keep going if we're not allowing interaction below this.
            // Setting newBlock will make sure we cancel the rest of the behaviors.
            final boolean wasBlocking = lp.didBlockInteraction();
            final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
            newBlock = isBlocking && !wasBlocking;
            if (isBlocking && !newBlock) {
                // Stop here since we don't have anything more to cancel - we already did
                // when the behavior first started blocking things below this point.
                break;
            }
        }

        topmostChildList.clear();

        return intercepted;
    }
複製代碼

能夠看到的是,若是 CoordinatorLayout 內部若是沒有被攔截,那麼它會傳遞觸摸信息給 Behavior,若是有 Behavior 須要攔截這樣的動做,那麼就交給 Behavior,若是沒有的話,它就會走傳統的 ViewGroup 處理觸摸事件的流程。

自此,CoordinatorLayout 絕大多數細節已經討論完成。

總結

若是你是從頭看到這裏,我不知道你有沒有這種感受,像探索同樣,經歷了很長一段時間,順着一條條線索,焦急、糾結,最終走出了一條道路。回首溯望,也許會有種風輕雲淡的感受。

這篇文章洋洋灑灑已經有千字以上了,由於篇幅過長,爲了防止遺忘。如今能夠將文章細節總結以下:

  1. CoordinatorLayout 是一個普通的 ViewGroup,它的佈局特性相似於 FrameLayout。
  2. CoordinatorLayout 是超級 FrameLayout,它比 FrameLayout 更強悍的緣由是它能與 Behavior 交互。
  3. CoordinatorLayout 與 Behavior 相輔相成,它們一塊兒構建了一個美妙的交互系統。
  4. 自定義 Behavior 主要有 2 個目的:1 肯定一個 View 依賴另一個 View 的依賴關係。2 指定一個 View 響應嵌套滑動事件。
  5. 肯定兩個 View 的依賴關係,有兩種途徑。一個是在 Behavior 中的 layoutDepentOn() 返回 true。另一種就是直接經過 xml 錨定一個 View。當被依賴方尺寸和位置變化時,Behavior 中的 onDependentViewChanged 方法會被調用。若是在這個方法中改變了主動依賴的那個 view 的尺寸或者位置信息,應該在方法最後 return true。
  6. 嵌套滑動分爲 nested scroll 和 fling 兩種。Behavior 中相應的 View 是否接受響應由 onStartNestedScroll() 返回值決定。通常在 onNestedPreScroll() 處理相應的 nested scroll 響應,在 onPreFling 處理 fling 事件。可是這個不絕對,根據實際狀況決定。
  7. NestedScrollView 可以產生嵌套滑動事件是由於它本質上是一個 NestedScrollingChild 對象,而 CoordinatorLayout 可以響應是由於它本質上是一個 NestedScrollingParent 對象。
  8. Behavior 是一種插件機制,若是沒有 Behavior 的存在,CoordinatorLayout 和普通的 FrameLayout 無異。Behavior 的存在,能夠決定 CoordinatorLayout 中對應的 childview 的測量尺寸、佈局位置、觸摸響應。

最後,回到文章最開始的地方。咱們已經熟悉了 CoordinatorLayout,NestedScrollView 也瞭解一點點。而對於 AppbarLayout 咱們還不瞭解。可是以前的恐慌卻不存在了,由於瞭解到 Behavior 的機制,咱們能夠知道 CoordinatorLayout 並非必定要和 AppBarLayout 或者 FloatButton 一塊兒配合使用,它是獨立的,拋開它們,咱們經過自定義 Behavior 也能夠實現很是炫麗的交互效果。

而系統自定義的 Behavior 能夠給開發者提供了許多場景的便利與下降開發難度。

不要重複造輪子。但不表明咱們不須要去了解輪子。

接下來,我將會一一學習 Android Support Design 這個庫中其它有意思的類,如 AppBarLayout、CollapsingToolbarLayout、FloatingActionButton 等等,固然,必不可少的是與它們配合使用的各種 Behavior,如很是牛逼的 AppBarLayout.ScrollingViewBehavior 和 BottomSheetBehavior 等等。

這個精彩世界值得咱們探索。

源碼github地址

相關文章
相關標籤/搜索