Android CoordinatorLayout Behavior

       Behavior是Android Support Design库里面新增的布局概念,主要的作用是用来协调CoordinatorLayout里面直接Child Views之间交互行为的。

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

       既然Behavior是用来协调CoordinatorLayout直接Child View的交互行为的。那Behavior是怎么工作的呢,这个也是我们本文的重点。我们准备从以下四条线路来做简单的分析。

  1. Behavior的测量和布局。(Behavior里面onMeasureChild、onLayoutChild函数)

  2. Behavior的普通触摸事件。(Behavior里面的onInterceptTouchEvent,onTouchEvent函数)

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

  4. Behavior的依赖关系。(Behavior里面的layoutDependsOn、onDependentViewChanged、onDependentViewRemoved函数)

       CoordinatorLayout直接Child View的LayoutParam里面的Behavior是怎么实例化得到.有三种方式:第一种,注解设置,类似@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)的形似;第二种,java代码设置;第三种,app:layout_behavior来设置.关于Behavior的实例化这里我们就不展开来讲,有兴趣的可以参考CoordinatorLayout里Behavior简单分析里面Behavior对象是怎么被实例化的.

第一种注解方式的使用来设置默认Behavior的.

一、Behavior的测量和布局

       Behavior可以引导CoordinatorLayout的直接Child View 进行测量和布局。CoordinatorLayout需要进行measure、layout的时候,都会通过Behavior询问该Behavior对应的View是否需要进行相应的测量和布局操作,如果不需要,就进行默认的行为。如果需要则按照Behavior里面编写的规则来测量和布局。这里我们只需要关注Behavior类的onMeasureChild()、onLayoutChild()两个函数。

       我们以一个具体的例子来简单的解释下Behavior怎么引导CoordinatorLayout的直接Child View 进行测量和布局的.在上一篇文章Android Design Support Library 控件的使用中有一个CoordinatorLayout + RecyclerView(ViewPager里面放置的是RecyclerView) + AppBarLayout 实现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"给设置了AppBarLayout.ScrollingViewBehavior.这样CoordinatorLayout两个直接子View都有对应的Behavior了.从界面结果出咱也能看到刚进入界面的时候ViewPager是在AppBarLayout的下面的.咱们就分析分析他是怎么做到的.肯定和测量和布局相关,那出发点肯定是CoordinatorLayout类的onMeasure()和onLayout().

CoordinatorLayout类onMeasure()函数

    @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);
            }
            ......
        }
        ......
    }

       分析可以发现如果对应的子View有对应的Behavior的时候,会先去调用Behavior里面的onMeasureChild()看Behavior有没有制定自己的测量方式.这下咱就的进入ViewPager对应的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);
            }
        }
    }

       同样分析可以得到有对应的Behavior就先进入到Behavior的onLayoutChild()方法了.ViewPager设置的AppBarLayout.ScrollingViewBehavior的onLayoutChild()方法里面获取得到AppBarLayout的区域,之后把ViewPager布局layout到AppBarLayout的下面.

       这样咱们以一个简单的例子对Behavior的测量和布局做了一个非常简单的分析.里面很多地方也没有去深究.如果大家有什么疑问的话,可以留言.在能力范围之内的都会尽力为大家解答的.

二、Behavior的普通触摸事件

       Behavior的普通触摸事件主要和Behavior里面的onInterceptTouchEvent()和onTouchEvent()两个函数相关.最终的目的也就是想把对应的触摸时间传递到Behavior对应的View里面去,让View做一些相应的处理.

       父布局CoordinatorLayout产生的onInterceptTouchEvent,onTouchEvent事件都会先送到Behavior的onInterceptTouchEvent()和onTouchEvent()里面,让去问问Behavior对应的View要不要处理.你要处理就先给你处理.你不处理才轮到CoordinatorLayout来处理.关于这部分的内容之前有写过一个文章.我们就不展开讨论了.有兴趣的可以参考下CoordinatorLayout里Behavior简单分析里面Behavior的onInterceptTouchEvent + onTouchEvent一部分的分析.

三、Behavior的嵌套NestedScrolling触摸事件

       关于Behavior嵌套滑动主要涉及Behavior里面的onStartNestedScroll(), onNestedScrollAccepted(), onStopNestedScroll(), onNestedScroll(), onNestedPreScroll(), onNestedFling(), onNestedPreFling() 函数.

       这里我们多次提到了嵌套滑动,有兴趣的可以参考我之前写的Android 嵌套滑动分析一文的简单分析.

       Behavior的嵌套NestedScrolling事件,大部分情况下是这样的.CoordinatorLayout里面另一个子View产生了嵌套滑动事件,这个事件先传递到CoordinatorLayout,然后CoordinatorLayout在把这个嵌套事件过渡到Behavior里面去.之后在让Beahaior对应的View按照实际情况做不同的处理.同样关于这部分内容的具体分析,有兴趣的可以参考下之前写的CoordinatorLayout里Behavior简单分析里面Behavior的onStartNestedScroll + onNestedScrollAccepted + onStopNestedScroll + onNestedScroll + onNestedPreScroll + onNestedFling + onNestedPreFling。嵌套滑动引起的变化部分的简单分析.

       同样为了加深理解,这里还是以上文CoordinatorLayout + RecyclerView(ViewPager里面放置的是RecyclerView) + AppBarLayout 实现AppBarLayout里面Toolbar的收缩和展开效果图的例子来做一个简单的说明.这也是ViewPager里面为什么一定要放置实现了NestedScrollingChild2接口的View.这里ViewPager里面放了RecyclerView(RecyclerView实现了NestedScrollingChild接口).当RecyclerView有对应的NestedScrollingChild滑动的时候,都会先传递到CoordinatorLayout里面对应函数里面去,然后CoordinatorLayout又会原封不动的传递到Behavior对应的onStartNestedScroll(), onNestedScrollAccepted(),onStopNestedScroll(),onNestedScroll(), onNestedPreScroll(),onNestedFling(),onNestedPreFling()的函数里面去.换句话说就是传递到了AppBarLayout对应的AppBarLayout.Behavior里面去.在里面让AppBarLayout对某个View的上移和下移的处理.

四、Behavior的依赖关系

       关于Behavior依赖关系对应Behavior里面的layoutDependsOn(), onDependentViewChanged(),onDependentViewRemoved()这三个函数.

       Behavior的依赖指的是当前Behavior对应的View依赖于哪个View.当依赖的View有变化的时候.会调用Behavior里面对应的函数.然我们对Behavior对应的View做相应的处理.同样关于这一部分的具体分析可以参考之前写的CoordinatorLayout里Behavior简单分析里面Behavior的layoutDependsOn + onDependentViewChanged + onDependentViewRemoved。View引起的变化部分.这里我们就不重新拿出来讲了,而且里面有一个简单的例子.

       为了加深理解,咱们还是以上文提到的CoordinatorLayout + RecyclerView(ViewPager里面放置的是RecyclerView) + AppBarLayout 实现AppBarLayout里面Toolbar的收缩和展开效果图的例子来做一个简单的说明哈,其实在这个里面ViewPager会依赖AppBarLayout的变化.为什么这么说呢.看ViewPager对应的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函数中ViewPager也会跟着AppBarLayout的移动而移动.

五、Behavior的具体使用

5.1 BottomSheetBehavior的使用

       BottomSheetBehavior:实现底部弹出框的一个Behavior,注意BottomSheetBehavior一定要配合CoordinatorLayout一起使用才有效果。

BottomSheetBehavior对应的View的状态:

状态 解释
STATE_EXPANDED bottom sheet 处于完全展开的状态:当bottom sheet的高度低于CoordinatorLayout容器时,整个bottom sheet都可见;或者CoordinatorLayout容器已经被bottom sheet填满
STATE_COLLAPSED 折叠状态(默认), bottom sheets只在底部显示一部分布局。显示高度可以通过 app:behavior_peekHeight 设置
STATE_DRAGGING 过渡状态,此时用户正在向上或者向下拖动bottom sheet
STATE_SETTLING 视图从脱离手指自由滑动到最终停下的这一小段时间
STATE_HIDDEN 默认无此状态(需要通过app:behavior_hideable 启用此状态),启用后用户将能通过向下滑动完全隐藏 bottom sheet

BottomSheetBehavior属性设置

属性 解释
app:behavior_hideable bottom sheet是否可以完全隐藏,默认为false
app:behavior_peekHeight bottom sheet为STATE_COLLAPSED(折叠)状态的时残留的高度
app:behavior_skipCollapsed 是否跳过STATE_COLLAPSED状态

       BottomSheetBehavior有两种实现方式,一个之直接嵌套在布局里面,一个是通过dialog的方式弹出.两种使用方式都不难.所以我们也就以一个具体的实例来说明.效果图如下:

BottomSheet.gif
5.2 自定义Behavior

       关于自定义Behavior,我们也实现了两个简单的效果.

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);
        }
    }

    /**
     * 这里有优化的空间,这里纯粹的去取了第一个view的measure height 有点限制的太死了
     */
    private int getFirstChildHeight(CoordinatorLayout coordinatorLayout) {
        return coordinatorLayout.getChildAt(0).getMeasuredHeight();
    }

}

       关于Behavior所要想分享的东西就这些了,如果后面自定义Behavior实现的特别有意思的效果也会第一时间分享给大家.最后上文涉及的所有实例的下载地址 https://github.com/tuacy/DesignWidget

推荐阅读更多精彩内容