Android CoordinatorLayout Behavior

? ? ? ?Behavior是Android Support Design庫里面新增的布局概念,主要的作用是用來協(xié)調(diào)CoordinatorLayout里面直接Child Views之間交互行為的。

特別要注意的點是Behavior只能作用于CoordinatorLayout的直接Child View.

? ? ? ?既然Behavior是用來協(xié)調(diào)CoordinatorLayout直接Child View的交互行為的。那Behavior是怎么工作的呢,這個也是我們本文的重點。我們準備從以下四條線路來做簡單的分析。

  1. Behavior的測量和布局。(Behavior里面onMeasureChild、onLayoutChild函數(shù))

  2. Behavior的普通觸摸事件。(Behavior里面的onInterceptTouchEvent,onTouchEvent函數(shù))

  3. Behavior的嵌套NestedScrolling觸摸事件。(Behavior里面的onStartNestedScroll、onNestedScrollAccepted、onStopNestedScroll、onNestedScroll、onNestedPreScroll、onNestedFling、onNestedPreFling函數(shù))

  4. Behavior的依賴關(guān)系。(Behavior里面的layoutDependsOn、onDependentViewChanged、onDependentViewRemoved函數(shù))

? ? ? ?CoordinatorLayout直接Child View的LayoutParam里面的Behavior是怎么實例化得到.有三種方式:第一種,注解設(shè)置,類似@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)的形似;第二種,java代碼設(shè)置;第三種,app:layout_behavior來設(shè)置.關(guān)于Behavior的實例化這里我們就不展開來講,有興趣的可以參考CoordinatorLayout里Behavior簡單分析里面Behavior對象是怎么被實例化的.

第一種注解方式的使用來設(shè)置默認Behavior的.

一、Behavior的測量和布局

? ? ? ?Behavior可以引導CoordinatorLayout的直接Child View 進行測量和布局。CoordinatorLayout需要進行measure、layout的時候,都會通過Behavior詢問該Behavior對應(yīng)的View是否需要進行相應(yīng)的測量和布局操作,如果不需要,就進行默認的行為。如果需要則按照Behavior里面編寫的規(guī)則來測量和布局。這里我們只需要關(guān)注Behavior類的onMeasureChild()、onLayoutChild()兩個函數(shù)。

? ? ? ?我們以一個具體的例子來簡單的解釋下Behavior怎么引導CoordinatorLayout的直接Child View 進行測量和布局的.在上一篇文章Android Design Support Library 控件的使用中有一個CoordinatorLayout + RecyclerView(ViewPager里面放置的是RecyclerView) + AppBarLayout 實現(xiàn)AppBarLayout里面Toolbar的收縮和展開效果圖的例子.如下圖所示

AppBarLayout.gif

并且他的布局文件如下

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center"
                android:textColor="@android:color/white"
                android:gravity="center"
                android:text="自定義標題"
                android:textSize="18sp" />

        </android.support.v7.widget.Toolbar>

        <android.support.design.widget.TabLayout
            android:id="@+id/tab_layout_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            style="@style/AppTheme.TabStyle"
            app:tabMode="scrollable"
            app:tabGravity="fill" />

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

    <android.support.v4.view.ViewPager
        android:id="@+id/page_collapsing"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

? ? ? ?最外層一個CoordinatorLayout布局,并且CoordinatorLayout里面有兩個直接的子View:AppBarLayout和ViewPager.其中AppBarLayout有一個默認的AppBarLayout.Behavior,同時ViewPager我們通過app:layout_behavior="@string/appbar_scrolling_view_behavior"給設(shè)置了AppBarLayout.ScrollingViewBehavior.這樣CoordinatorLayout兩個直接子View都有對應(yīng)的Behavior了.從界面結(jié)果出咱也能看到剛進入界面的時候ViewPager是在AppBarLayout的下面的.咱們就分析分析他是怎么做到的.肯定和測量和布局相關(guān),那出發(fā)點肯定是CoordinatorLayout類的onMeasure()和onLayout().

CoordinatorLayout類onMeasure()函數(shù)

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        for (int i = 0; i < childCount; i++) {
            final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            ......
            final CoordinatorLayout.Behavior b = lp.getBehavior();
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                                               childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                               childHeightMeasureSpec, 0);
            }
            ......
        }
        ......
    }

? ? ? ?分析可以發(fā)現(xiàn)如果對應(yīng)的子View有對應(yīng)的Behavior的時候,會先去調(diào)用Behavior里面的onMeasureChild()看Behavior有沒有制定自己的測量方式.這下咱就的進入ViewPager對應(yīng)的Behavior AppBarLayout.ScrollingViewBehavior里面的onMeasureChild()方法里面去瞧一瞧了,這里我們就不進去了.里面也就是一些正常的測量方法.測量完成接下來就是layout了.CoordinatorLayout類的onLayout()方法.

CoordinatorLayout類onLayout()方法

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        ......
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            ......
            final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            final CoordinatorLayout.Behavior behavior = lp.getBehavior();
            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }
        }
    }

? ? ? ?同樣分析可以得到有對應(yīng)的Behavior就先進入到Behavior的onLayoutChild()方法了.ViewPager設(shè)置的AppBarLayout.ScrollingViewBehavior的onLayoutChild()方法里面獲取得到AppBarLayout的區(qū)域,之后把ViewPager布局layout到AppBarLayout的下面.

? ? ? ?這樣咱們以一個簡單的例子對Behavior的測量和布局做了一個非常簡單的分析.里面很多地方也沒有去深究.如果大家有什么疑問的話,可以留言.在能力范圍之內(nèi)的都會盡力為大家解答的.

二、Behavior的普通觸摸事件

? ? ? ?Behavior的普通觸摸事件主要和Behavior里面的onInterceptTouchEvent()和onTouchEvent()兩個函數(shù)相關(guān).最終的目的也就是想把對應(yīng)的觸摸時間傳遞到Behavior對應(yīng)的View里面去,讓View做一些相應(yīng)的處理.

? ? ? ?父布局CoordinatorLayout產(chǎn)生的onInterceptTouchEvent,onTouchEvent事件都會先送到Behavior的onInterceptTouchEvent()和onTouchEvent()里面,讓去問問Behavior對應(yīng)的View要不要處理.你要處理就先給你處理.你不處理才輪到CoordinatorLayout來處理.關(guān)于這部分的內(nèi)容之前有寫過一個文章.我們就不展開討論了.有興趣的可以參考下CoordinatorLayout里Behavior簡單分析里面Behavior的onInterceptTouchEvent + onTouchEvent一部分的分析.

三、Behavior的嵌套NestedScrolling觸摸事件

? ? ? ?關(guān)于Behavior嵌套滑動主要涉及Behavior里面的onStartNestedScroll(), onNestedScrollAccepted(), onStopNestedScroll(), onNestedScroll(), onNestedPreScroll(), onNestedFling(), onNestedPreFling() 函數(shù).

? ? ? ?這里我們多次提到了嵌套滑動,有興趣的可以參考我之前寫的Android 嵌套滑動分析一文的簡單分析.

? ? ? ?Behavior的嵌套NestedScrolling事件,大部分情況下是這樣的.CoordinatorLayout里面另一個子View產(chǎn)生了嵌套滑動事件,這個事件先傳遞到CoordinatorLayout,然后CoordinatorLayout在把這個嵌套事件過渡到Behavior里面去.之后在讓Beahaior對應(yīng)的View按照實際情況做不同的處理.同樣關(guān)于這部分內(nèi)容的具體分析,有興趣的可以參考下之前寫的CoordinatorLayout里Behavior簡單分析里面Behavior的onStartNestedScroll + onNestedScrollAccepted + onStopNestedScroll + onNestedScroll + onNestedPreScroll + onNestedFling + onNestedPreFling。嵌套滑動引起的變化部分的簡單分析.

? ? ? ?同樣為了加深理解,這里還是以上文CoordinatorLayout + RecyclerView(ViewPager里面放置的是RecyclerView) + AppBarLayout 實現(xiàn)AppBarLayout里面Toolbar的收縮和展開效果圖的例子來做一個簡單的說明.這也是ViewPager里面為什么一定要放置實現(xiàn)了NestedScrollingChild2接口的View.這里ViewPager里面放了RecyclerView(RecyclerView實現(xiàn)了NestedScrollingChild接口).當RecyclerView有對應(yīng)的NestedScrollingChild滑動的時候,都會先傳遞到CoordinatorLayout里面對應(yīng)函數(shù)里面去,然后CoordinatorLayout又會原封不動的傳遞到Behavior對應(yīng)的onStartNestedScroll(), onNestedScrollAccepted(),onStopNestedScroll(),onNestedScroll(), onNestedPreScroll(),onNestedFling(),onNestedPreFling()的函數(shù)里面去.換句話說就是傳遞到了AppBarLayout對應(yīng)的AppBarLayout.Behavior里面去.在里面讓AppBarLayout對某個View的上移和下移的處理.

四、Behavior的依賴關(guān)系

? ? ? ?關(guān)于Behavior依賴關(guān)系對應(yīng)Behavior里面的layoutDependsOn(), onDependentViewChanged(),onDependentViewRemoved()這三個函數(shù).

? ? ? ?Behavior的依賴指的是當前Behavior對應(yīng)的View依賴于哪個View.當依賴的View有變化的時候.會調(diào)用Behavior里面對應(yīng)的函數(shù).然我們對Behavior對應(yīng)的View做相應(yīng)的處理.同樣關(guān)于這一部分的具體分析可以參考之前寫的CoordinatorLayout里Behavior簡單分析里面Behavior的layoutDependsOn + onDependentViewChanged + onDependentViewRemoved。View引起的變化部分.這里我們就不重新拿出來講了,而且里面有一個簡單的例子.

? ? ? ?為了加深理解,咱們還是以上文提到的CoordinatorLayout + RecyclerView(ViewPager里面放置的是RecyclerView) + AppBarLayout 實現(xiàn)AppBarLayout里面Toolbar的收縮和展開效果圖的例子來做一個簡單的說明哈,其實在這個里面ViewPager會依賴AppBarLayout的變化.為什么這么說呢.看ViewPager對應(yīng)的AppBarLayout.ScrollingViewBehavior里面

        @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            // We depend on any AppBarLayouts
            return dependency instanceof AppBarLayout;
        }

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

看到了吧,如果是AppBarLayout就依賴他.并且在onDependentViewChanged函數(shù)中ViewPager也會跟著AppBarLayout的移動而移動.

五、Behavior的具體使用

5.1 BottomSheetBehavior的使用

? ? ? ?BottomSheetBehavior:實現(xiàn)底部彈出框的一個Behavior,注意BottomSheetBehavior一定要配合CoordinatorLayout一起使用才有效果。

BottomSheetBehavior對應(yīng)的View的狀態(tài):

狀態(tài) 解釋
STATE_EXPANDED bottom sheet 處于完全展開的狀態(tài):當bottom sheet的高度低于CoordinatorLayout容器時,整個bottom sheet都可見;或者CoordinatorLayout容器已經(jīng)被bottom sheet填滿
STATE_COLLAPSED 折疊狀態(tài)(默認), bottom sheets只在底部顯示一部分布局。顯示高度可以通過 app:behavior_peekHeight 設(shè)置
STATE_DRAGGING 過渡狀態(tài),此時用戶正在向上或者向下拖動bottom sheet
STATE_SETTLING 視圖從脫離手指自由滑動到最終停下的這一小段時間
STATE_HIDDEN 默認無此狀態(tài)(需要通過app:behavior_hideable 啟用此狀態(tài)),啟用后用戶將能通過向下滑動完全隱藏 bottom sheet

BottomSheetBehavior屬性設(shè)置

屬性 解釋
app:behavior_hideable bottom sheet是否可以完全隱藏,默認為false
app:behavior_peekHeight bottom sheet為STATE_COLLAPSED(折疊)狀態(tài)的時殘留的高度
app:behavior_skipCollapsed 是否跳過STATE_COLLAPSED狀態(tài)

? ? ? ?BottomSheetBehavior有兩種實現(xiàn)方式,一個之直接嵌套在布局里面,一個是通過dialog的方式彈出.兩種使用方式都不難.所以我們也就以一個具體的實例來說明.效果圖如下:

BottomSheet.gif
5.2 自定義Behavior

? ? ? ?關(guān)于自定義Behavior,我們也實現(xiàn)了兩個簡單的效果.

5.2.1 上滑下滑的時候FloatingActionButton底部彈入或者彈出

效果圖

bottom.gif

Behavior

public class FabBottomInOutBehavior extends FloatingActionButton.Behavior {

    private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();

    private boolean mAnimatingOut = false;

    public FabBottomInOutBehavior() {
    }

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

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                       @NonNull FloatingActionButton child,
                                       @NonNull View directTargetChild,
                                       @NonNull View target,
                                       int axes,
                                       int type) {
        //需要垂直的滑動
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL ||
               super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
    }


    @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                               @NonNull FloatingActionButton child,
                               @NonNull View target,
                               int dxConsumed,
                               int dyConsumed,
                               int dxUnconsumed,
                               int dyUnconsumed,
                               int type) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
        if (dyConsumed > 0 && !mAnimatingOut) {
            //向上滑動
            animateOut(child);
        } else if (dyConsumed < 0) {
            //向下滑動
            animateIn(child);
        }
    }

    private void animateOut(final FloatingActionButton button) {
        ViewCompat.animate(button)
                  .translationY(button.getHeight() + getMarginBottom(button))
                  .setInterpolator(INTERPOLATOR)
                  .withLayer()
                  .setListener(new ViewPropertyAnimatorListener() {
                      public void onAnimationStart(View view) {
                          mAnimatingOut = true;
                      }

                      public void onAnimationCancel(View view) {
                          mAnimatingOut = false;
                      }

                      public void onAnimationEnd(View view) {
                          mAnimatingOut = false;
                      }
                  })
                  .start();
    }


    private void animateIn(FloatingActionButton button) {
        ViewCompat.animate(button).translationY(0).setInterpolator(INTERPOLATOR).withLayer().setListener(null).start();
    }

    private int getMarginBottom(View v) {
        final ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
        if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
            return ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
        }
        return 0;
    }
}
5.2.2 上滑的時候以覆蓋的方式蓋住頭部

效果圖

cover.gif

Behavior

public class HeaderCoverBehavior extends CoordinatorLayout.Behavior<View> {

    public HeaderCoverBehavior() {
    }

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

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

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        if (params != null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT) {
            child.layout(0, 0, parent.getWidth(), parent.getHeight());
            child.setTranslationY(getFirstChildHeight(parent));
            return true;
        }

        return super.onLayoutChild(parent, child, layoutDirection);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                       @NonNull View child,
                                       @NonNull View directTargetChild,
                                       @NonNull View target,
                                       int axes,
                                       int type) {
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                  @NonNull View child,
                                  @NonNull View target,
                                  int dx,
                                  int dy,
                                  @NonNull int[] consumed,
                                  int type) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        // 在這個方法里面只處理向上滑動
        if (dy < 0) {
            return;
        }
        float transY = child.getTranslationY() - dy;
        if (transY > 0) {
            child.setTranslationY(transY);
            consumed[1] = dy;
        }
    }

    @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                               @NonNull View child,
                               @NonNull View target,
                               int dxConsumed,
                               int dyConsumed,
                               int dxUnconsumed,
                               int dyUnconsumed,
                               int type) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
        // 在這個方法里只處理向下滑動
        if (dyUnconsumed > 0) {
            return;
        }
        float transY = child.getTranslationY() - dyUnconsumed;
        if (transY > 0 && transY < getFirstChildHeight(coordinatorLayout)) {
            child.setTranslationY(transY);
        }
    }

    /**
     * 這里有優(yōu)化的空間,這里純粹的去取了第一個view的measure height 有點限制的太死了
     */
    private int getFirstChildHeight(CoordinatorLayout coordinatorLayout) {
        return coordinatorLayout.getChildAt(0).getMeasuredHeight();
    }

}

? ? ? ?關(guān)于Behavior所要想分享的東西就這些了,如果后面自定義Behavior實現(xiàn)的特別有意思的效果也會第一時間分享給大家.最后上文涉及的所有實例的下載地址 https://github.com/tuacy/DesignWidget

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容