CoordinatorLayout布局和自定义Behavior

前言

Android引入了Material Design设计,并且为开发者提供了design支持库,库里包含了大量的材料设计相关的新布局。其中CoordinatorLayout布局最为重要,因为它提供内部子控件之间的各种协调效果,除了Google提供的Toobar控件和滚动内容的联动效果外,用户还可以自定义不同视图之间的behavior,现在就来学习一下这些酷炫的新效果。

实现效果

实现接口

CoordinatorLayout is a super-powered android.widget.FrameLayout FrameLayout.
CoordinatorLayout is intended for two primary use cases: As a top-level application decor or chrome layout. As a container for a specific interaction with one or more child views

Android开发文档里介绍CoordinatorLayout是FrameLayout的增强版,但是它并非继承自FrameLayout而是直接继承了ViewGroup。协调者布局通常被用作以下两种目的,作为应用程序的最高层装饰布局或者作为和一个或多个子视图有特殊交互效果的布局容器。协调者布局的交互效果都是通过behavior这种机制实现多个不同视图之间的协调一致。

Android应用的界面通常被分成两大部分,上面的是各种标题栏,下面是内容布局。如果内容布局是可滚动的内容,标题栏会因为无法跟随着内容部分滚动而遮挡内容减少内容有效展示界面。CoordinatorLayout内置的appbar_layout_behavior正好能够支持内容布局和上方的联动效果。AppBarLayout是一个继承自LinearLayout的新布局,默认情况下方向是竖直方向,它会为自己的直接子元素增加app:layout_scrollFlags效果,元素所有合法的值如下:

滚动值 效果
scroll 子视图随着外部的内容布局滚动而一起滚动
enterAlways 子视图在内容布局向下滚动时一定会出现顶部
enterAlwaysCollapsed 需要配合minHeight使用,当用户向下滚动内容时首先出现的是minHeight高度的子视图,随后内容布局滚动到和minHeight一样高的距离后在跟着滚动出其它的子视图
exitUntilCollapsed 用户向上滚动内容布局最后还会留下minHeight高度的子视图
snap 用户滚动如果子视图展示超过50%则会继续展示全部子视图,如果没超过50%,则隐藏展示的子布局

上面的AppBarLayout支持的滚动效果虽然已经可以满足部分需求,但是用户有时还需要能够精确的指定状态栏里的内容压缩(Collapse)的动画效果,压缩效果令整体的滚动切换过程更加自然,提升用户体验。CollapsingToolbarLayout布局提供了这种压缩效果,它继承于FrameLayout,内部的子视图可以按照FrameLayout的方式布局,不过需要注意的是如果用户需要为Toolbar设置固定模式,一定要把Toolbar放在CollapsingToolbarLayout的最前方,确保不会被其他子视图遮挡。和AppBarLayout类似,CollapsingoolbarLayout也通过为子视图添加属性app:layout_collapseMode来实现压缩效果:

模式值 效果
parallax 视差模式,压缩的过程中用户可以设置子视图滚动速度通常比内容布局滚动速度小,这样就能看到压缩过程了
pin 固定模式,被压缩的子视图最后被固定在顶部

app:layout_collapseParallaxMultiplier属性能够在视差模式影响滚动速度,设置的值在0~1之间,值越大表明视差效果越明显。

实现过程

最开始需要引入design设计包,在Activity的布局文件中添加CoordinatorLayout为根部局,在内部增加AppBarLayout控制顶部的滚动效果,在AppBarLayout内部增加CollapsingToolbarLayout负责压缩效果实现,最后在其内部增加需要压缩展示的ImageView和定位用的FrameLayout。下面放置支持嵌套滚动的RecyclerView做内容布局,为它设置appbar_layout_behavior让它在滚动的时候通知到AppBarLayout布局。


<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:orientation="vertical"
    tools:context="com.example.design.CoordinatorActivity">

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

        <android.support.design.widget.CollapsingToolbarLayout
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:contentScrim="@color/colorPrimary"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ImageView
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.9"
                android:src="@drawable/mountain"
                android:layout_width="match_parent"
                android:layout_height="200dp" />

            <FrameLayout
                android:id="@+id/layout"
                android:layout_gravity="bottom|center_horizontal"
                app:layout_collapseMode="parallax"
                android:layout_width="match_parent"
                android:layout_height="100dp">
            FrameLayout>

        android.support.design.widget.CollapsingToolbarLayout>

    android.support.design.widget.AppBarLayout>

    
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    android.support.v7.widget.RecyclerView>

    
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        app:theme="@style/ThemeOverlay.AppCompat.Dark"
        app:title=""
        app:layout_anchor="@id/layout"
        android:layout_width="match_parent"
        android:layout_height="54dp">

    android.support.v7.widget.Toolbar>

    
    <TextView
        android:id="@+id/textview"
        android:textSize="25sp"
        android:textColor="@color/colorAccent"
        android:background="@color/colorPrimary"
        android:textStyle="bold"
        android:gravity="center"
        app:layout_behavior="@string/share_behavior"
        android:text="@string/name_list"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

    
    <com.example.design.widget.CircleImageView
        android:id="@+id/image"
        android:scaleType="centerCrop"
        app:layout_behavior="@string/drawable_behavior"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="150dp"
        android:src="@drawable/waterfall"
        android:layout_width="80dp"
        android:layout_height="80dp" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/floatButton"
        android:src="@drawable/ic_camera_black_24dp"
        android:background="@color/colorAccent"
        android:layout_gravity="bottom|right"
        android:layout_margin="15dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

android.support.design.widget.CoordinatorLayout>

通过上面的布局就可以实现简单的顶部压缩效果展示,现在考虑顶部从上向下出现的标题如何实现。从上向下这个动画效果完全可以使用属性动画translationY来实现,不过如何获得压缩动画的执行状态呢,上面设置了Toolbar锚点在CollapsingToolbarLayout里的FrameLayout上,Toolbar会随着FrameLayout的压缩过程逐渐改变自己的位置信息,可以为Title布局增加自定义Behavior监听Toolbar的位置更新信息,在Toolbar改变的过程中更新Title布局的位置。自定义Behavior需要扩展自CoordinatorLayout.Behavior泛型类,其中的泛型就是需要改变的布局类型,title布局类型是TextView类型,所以这个Behavior实现如下:

 public static final class MessageBehavior extends CoordinatorLayout.Behavior<TextView> {
    // 记录Toolbar的顶部位置
    private int start = 0;

    // 构造函数
    public MessageBehavior() {
    }

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

    // 判断dependency的类型
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
        return dependency instanceof Toolbar;
    }

    // 当被依赖的对象也就是被监听的对象,这里就是Toolbar改变时回调,设置title布局的translationY
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
        if (start == 0) {
            start = (int) dependency.getY();
        }
        float ratio = dependency.getY() / start;
        child.setY(-child.getMeasuredHeight() * ratio);
        return true;
    }
}

在Behavior中将被监视的视图称作Dependency,也就是依赖视图,根据依赖视图做更新的视图称作Child,setY查看源码会发现其实就是通过设置translationY来实现视图的滚动。

再来思考如何实现图片的跟随缩放效果,上面通过Toolbar获取压缩效果执行状态ratio,现在可以根据这个ratio设置图片的位置和大小,实现代码如下:

public static final class DrawableBehavior extends CoordinatorLayout.Behavior {
    private int start = 0;
    private int startX = 0;
    private int startY = 0;
    private int endX = CommonUtils.dp2px(10);
    private int endY = CommonUtils.dp2px(10);
    private int endSize = CommonUtils.dp2px(30);
    private int startSize = 0;
    public DrawableBehavior() {
    }

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

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

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, ImageView child, View dependency) {
        if (start == 0) {
            start = (int) dependency.getY();
            startX = (int) child.getX();
            startY = (int) child.getY();
            startSize = CommonUtils.dp2px(80);
        }
        float ratio = dependency.getY() / start;

        // 设置图片的位置
        child.setX(startX + (1 - ratio) * (endX - startX));
        child.setY(startY + (1 - ratio) * (endY - startY));

        // 设置图片的宽高
        child.getLayoutParams().width = (int) (startSize + (1 - ratio) * (endSize - startSize));
        child.getLayoutParams().height = (int) (startSize + (1 - ratio) * (endSize - startSize));
        child.requestLayout();
        return true;
    }
}

查看所有实现代码请点击查看代码。

你可能感兴趣的:(Android学习)