Material Design 系列(一)- CollapsingToolbarLayout 和 AppBarLayout

本贴最后更新于 2037 天前,其中的信息可能已经水流花落

1. 什么是 CoordinatorLayout

CoordinatorLayout 是 Android 官方在 Design 包提供的控件,来自官方的解释是:

CoordinatorLayout is a super-powered FrameLayout

它主要用于两个方面:

  • 当做普通的 FrameLayout 作为根布局使用
  • 作为一个或者多个子 View 进行复杂交互的容器

CoordinatorLayout 为我们提供了一个叫做 Behavior 的东西,我们基本上的复杂交互都是使用 Behavior 来协调完成,本文就实现一个简单的交互效果。

先看看本文的最终成果:

toolbar 和 header 的移动完整例子

2. 如何使用 CoordinatorLayout

2.1 添加依赖

这里我选择的是 26.0.2 版本,至于为什么选择这个,是因为我使用的最多的就是这个版本,暂时没有发现一些其他的什么问题。

dependencies{
    implementation 'com.android.support:design:26.0.2'
}

自从 Android Studio3.0 以后,基本上都是改用了 implementation,而不是使用 compile ,主要原因是使用 implementation 不会产生依赖传递,这对于我们来说,减少了很多麻烦,比方说以前解决的重复依赖版本的问题。

2.2 布局中使用 CoordinatorLayout

很简单的写了一下,现在只要是当做简单的 FrameLayout 来使用,内部只放了一个 TextView,详情看以下代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="android.of.road.com.course1.CreateCoordinatorLayoutActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="20dp"
        android:text="Hello CoordinatorLayout"
        android:textSize="20sp" />

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

我们可以运行起来看看效果

Hello
CoordinatorLayout 的第一点作用就已经展现出来了,在我们要讲解它的第二点功能之前,我们需要准备一些预备知识。

3. 什么是 AppBarLayout

AppBarLayout 是一个垂直的 LinearLayout, 它实现了很多在 material designs 设计中提出的概念性交互功能,也就是【滚动手势】。

4. 如何使用 AppBarLayout

4.1 例子 1

AppBarLayout 也是 design 包中的一个控件,所以在这里就不需要再添加依赖了。我们可以通过给它的子 View 进行 setScrollFlags(int)或者直接在 xml 中增加属性 app:layout_scrollFlags 来设置它子 View 的滚动行为。
需要注意的是,AppBarLayout 需要配合 CoordinatorLayout 进行使用,如果只是放到普通的 ViewGroup 中使用的话将无法实现它的效果。

<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"
    tools:context="android.of.road.com.course1.CreateAppBarLayoutActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|enterAlways"
            app:navigationIcon="@drawable/ic_arrow_back_white_24dp" />

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

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="500dp">

        </LinearLayout>

    </android.support.v4.widget.NestedScrollView>

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

随着内容的滑动,ToolBar 也跟随着滑动,详情看效果图:

CreateCoordinatorLayoutActivity

4.2 app:layout_scrollFlags 介绍

  • enterAlways:向上滚动时视图将变为可见。
  • enterAlwaysCollapsed:通常,当仅使用 enterAlways 时,工具栏将在向下滚动时继续扩展。如果声明了 enterAlways 并指定了 minHeight,则还可以指定 enterAlwaysCollapsed。使用此设置时,您的视图将仅显示在此最小高度。仅当滚动到达顶部时,视图才会扩展到其完整高度
  • exitUntilCollapsed:当设置滚动标志时,向下滚动通常会导致整个内容移动。通过指定 minHeight 和 exitUntilCollapsed,将在其余内容开始滚动并退出屏幕之前达到工具栏的最小高度
  • Snap:使用此选项将确定仅在部分缩小视图时要执行的操作。如果滚动结束并且视图大小已减小到原始视图的小于 50%,则此视图将返回其原始大小。尺寸大于其尺寸的 50%,它将完全消失。

4.3 Behavior

Behavior 这个名词看着陌生,但事实上我们经常简单,如:app:layout_behavior="@string/appbar_scrolling_view_behavior",这个一般是出现在使用 Android Stuido 创建 Activty 的时候自动创建的,他的作用是让使用这个属性的 View 在 appbar 下面滚动。而 Behavior 主要的使用方式其实是通过反射来实现的,我们在 layout_behavior 中并没有直接进行引用,而是写了包名 + 类名,所以 Behavior 是不能够被混淆的。

5. 例子

前面讲了那么都得概念,关于 CollapsingToolbarLayout 的使用在网络上已经写得烂大街了,所以在这里也就不做太多的概述,关于 layout_scrollFlags 的使用和介绍也是一样的,所以我们直接上手代码吧。
下面的小例子是为了实现一个简单的透明度渐变的 toolbar 和 header 的移动。先看看效果图吧:

toolbar 和 header 的移动完整例子

5.1 分析

从图中可以看到,我们的标题栏是随着上下滑动而透明度渐变,头像在展开状态下处于居中状态,随着慢慢的滑动合上,头像会改变 X 轴和 Y 轴的坐标,最终移动到返回键的旁边。

5.2 开始编码

我们采用的做法是用 AppBarLayout 包裹一个 CollapsingToolbarLayout,CollapsingToolbarLayout 中放我们的背景图片,随着滑动收缩和关闭图片,下面是一个 NestedScrollView,里面放 LinearLayout 和一大堆的 CardView。

activity_transfer_header 中的布局

<?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"
    tools:context=".TransferHeaderActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/mCollapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/iv"
                android:layout_width="match_parent"
                android:layout_height="240dp"
                android:scaleType="fitXY"
                android:src="@mipmap/design"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.9" />
				
        </android.support.design.widget.CollapsingToolbarLayout>
		
    </android.support.design.widget.AppBarLayout>
	
    <include layout="@layout/layout_tr_content" />
	
</android.support.design.widget.CoordinatorLayout>

其中我们 CollapsingToolbarLayout 的 layout_scrollFlags 使用的是 scroll 和 exitUntilCollapsed,scroll 是必需要设置的,不设置将无法滑动,exitUntilCollapsed 可以让 CollapsingToolbarLayout 退出关闭。

ImageView 的 layout_collapseMode 为 parallax,代表 ImageView 滑动会有一个视差滚动的效果,而视差滚动的比值(layout_collapseParallaxMultiplier)是 0.9。需要注意的是 CollapsingToolbarLayout 内部必需有一个铺满高度的 VIew 来做参考坐标,自定义 Behavior 才能够起作用。

layout_tr_content 中的内容

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView 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:scrollbars="none"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:showIn="@layout/activity_translucent_behavior">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:layout_marginBottom="40dp"
            android:layout_marginLeft="40dp"
            android:layout_marginRight="40dp"
            app:cardElevation="3dp" />

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:layout_marginBottom="40dp"
            android:layout_marginLeft="40dp"
            android:layout_marginRight="40dp"
            app:cardElevation="3dp" />
    </LinearLayout>

</android.support.v4.widget.NestedScrollView>

好了,我们运行起来看看效果吧:

效果 1

诶? 有没有发现效果不一样? 我们运行起来的是这样:

效果一

而我们效果图中的是这样:

效果二

其实只需要在 NestedScrollView 中加上一个属性 behavior_overlapTop,它会向上缩进赋值的高度:

加上 behavior_overlapTop

5.3 实现透明 Toolbar

创建 TranslucentBehavior 继承至 CoordinatorLayout.Behavior。我们在 Behavior 中计算移动的 Y 轴和总高度的比例,然后计算 alpha 通道的值,设置 Toolbar 的背景颜色。

具体代码实现

public class TranslucentBehavior extends CoordinatorLayout.Behavior<Toolbar> {

    /**标题栏的高度*/
    private int mToolbarHeight = 0;

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

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

    /**
     * 必须要加上  layout_anchor,对方也要layout_collapseMode才能使用
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, Toolbar child, View dependency) {

        // 初始化高度
        if (mToolbarHeight == 0) {
            mToolbarHeight = child.getBottom() * 2;//为了更慢的
        }
        //
        //计算toolbar从开始移动到最后的百分比
        float percent = dependency.getY() / mToolbarHeight;

        //百分大于1,直接赋值为1
        if (percent >= 1) {
            percent = 1f;
        }

        // 计算alpha通道值
        float alpha = percent * 255;


        //设置背景颜色
        child.setBackgroundColor(Color.argb((int) alpha, 63, 81, 181));

        return true;
    }
}

在 xml 中进行引用:

<?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"
    tools:context=".TransferHeaderActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/mCollapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/iv"
                android:layout_width="match_parent"
                android:layout_height="240dp"
                android:scaleType="fitXY"
                android:src="@mipmap/design"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.9" />

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

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

    <include layout="@layout/layout_tr_content" />
   <!--layout_anchor iv 才起作用-->
    <android.support.v7.widget.Toolbar
        android:id="@+id/tl"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/iv"
        app:layout_behavior="android.of.road.com.behavior.TranslucentBehavior"
        app:navigationIcon="@drawable/ic_arrow_back_white_24dp" />

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

需要注意的是:

  • layout_behavior 中放的的是全路径,错一个都不能正常工作
  • Toolbar 需要放在 content 之后

来看看效果吧:

toolbar 和 header 的移动-透明标题

完美达成!!

5.4 头像移动

创建 TransferHeaderBehavior 继承 CoordinatorLayout.Behavior,传入泛型是 ImageView,这个 ImageView 就是我们的参考坐标 View。
实现原理是:

  • 先计算出 header 的 X 轴位置:父 View 的宽度/2 - header 的宽度/2
  • Y 轴为父 View 的高度 - header 的高度
  • 不断使用滑动的父 Y 轴的距离来计算算 header 的 X 轴百分比和 Y 轴百分比
  • 不断的更改 X 轴的距离即可

具体实现看代码:

public class TransferHeaderBehavior extends CoordinatorLayout.Behavior<ImageView> {

    /**
     * 处于中心时候原始X轴
     */
    private int mOriginalHeaderX = 0;
    /**
     * 处于中心时候原始Y轴
     */
    private int mOriginalHeaderY = 0;


    public TransferHeaderBehavior(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) {
        // 计算X轴坐标
        if (mOriginalHeaderX == 0) {
            this.mOriginalHeaderX = dependency.getWidth() / 2 - child.getWidth() / 2;
        }
        // 计算Y轴坐标
        if (mOriginalHeaderY == 0) {
            mOriginalHeaderY = dependency.getHeight() - child.getHeight();
        }
        //X轴百分比
        float mPercentX = dependency.getY() / mOriginalHeaderX;
        if (mPercentX >= 1) {
            mPercentX = 1;
        }
        //Y轴百分比
        float mPercentY = dependency.getY() / mOriginalHeaderY;
        if (mPercentY >= 1) {
            mPercentY = 1;
        }

        float x = mOriginalHeaderX - mOriginalHeaderX * mPercentX;
        if (x <= child.getWidth()) {
            x = child.getWidth();
        }
        // TODO 头像的放大和缩小没做

        child.setX(x);
        child.setY(mOriginalHeaderY - mOriginalHeaderY * mPercentY);
        return true;
    }
}

在 xml 中引用

<?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"
    tools:context=".TransferHeaderActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/mCollapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/iv"
                android:layout_width="match_parent"
                android:layout_height="240dp"
                android:scaleType="fitXY"
                android:src="@mipmap/design"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.9" />

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

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

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

    <android.support.v7.widget.Toolbar
        android:id="@+id/tl"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/iv"
        app:layout_behavior="android.of.road.com.behavior.TranslucentBehavior"
        app:navigationIcon="@drawable/ic_arrow_back_white_24dp" />
    <!--layout_anchor iv 才起作用-->

    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_gravity="center_horizontal"
        android:elevation="5dp"
        android:src="@mipmap/default_header"
        app:layout_anchor="@id/iv"
        app:layout_behavior="android.of.road.com.behavior.TransferHeaderBehavior" />

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

运行查看效果:

toolbar 和 header 的移动完整例子

完美达成!!

6. 最后

TransferHeaderBehavior中遗留了一个todo:头像的缩小,这个留着下一章实现。

未完待续、敬请期待!
免为其难的关注一下公众号吧!!

FullScreenDeveloper

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1090 引用 • 3467 回帖 • 298 关注
  • Behavior
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...