小白7码-Android原生开发皮毛系列-CoordinatorLayout,布局中的合唱团

1,180 阅读8分钟

好久没有开启Android原生开发皮毛系列新篇章,今天小白给大家安利一款新的布局控件CoordinatorLayout,即协调布局控件。要了解CoordinatorLayout的作用,我们不妨想象一下我们在剧院里聆听一场音乐的盛宴。男女高音,钢琴,小提琴伴奏等共同合作演奏美妙的音乐。而CoordinatorLayout就扮演着如音乐演奏中协调各方根据乐谱和节奏共同行动合作的角色。

在小白看来CoordinatorLayout与其说是一款布局控件,它更像是一种行为规范和约束。事实上它并不显示地处理内部控件的布局和位置。相反它更关注内部的行为及其同步。我们使用CoordinatorLayout比较常用的一种应用就是同步滚动。先让我们介绍一下CoordinatorLayout家族的吉祥四宝:NestedScrollView,AppbarLayout, CollapsingToolBarLayout及ToolBar。CoordinatorLayout需要借助这四个宝贝才能真正显示起威力。我们下面会通过具体的例子做展开。

                                        

                                                 图1 CoordinatorLayout应用示意

如上图所示,CoordinatorLayout布局通常会包含两个部分:主体部分一般会展示列表或者大段落文本等,通常包含在NestedScrollView或者RecycleView中。附属部分一般展示自定义头部信息,通常包含在AppBarLayout里面并和CollapsingToolBarLayout和ToolBar一起使用。

而CoordinatorLayout的原理就是定义了一个行为约束叫做CoordinatorLayout.Behavior。而AppBarLayout实现了一种CoordinatorLayout.Behavior叫做ScrollViewBehavior。而NestedScrollView通过app:layout_behavior属性调用AppBarLayout的ScrollViewBehavior和AppBarLayout建立了一种链接,这种链接具现化的表现就是同步滚动。而链接的成立的前提就是需要在CoordinatorLayout内。这有点类似于订阅模式的机制。那么让我们看一个更加实际的例子:

                            

                                          图2 CoordinatorLayout实践

图2是一个关于书籍内容概要和目录浏览的页面。包含两部分内容:Android Studio开发实战这本书的出版信息和书籍的内容概要。

合唱团主要成员就位

CoordinatorLayout作为协调布局,在内部成员间架构了一条桥梁,协调成员间的行为。那么作为被协调的对象总是有主次之分。就如同合唱团的成员都以男女高音为主,而钢琴或者弦乐伴奏为辅。那么在CoordinatorLayout的合唱团中,我们通常会把大片的篇幅留给我们的主要成员 - NestedScrollView。在上面的实际的例子中,指的是Android Studio开发实战这本书的内容概要部分,代码如下:

<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
>

    <TextView
        android:layout_marginTop="@dimen/big_spacing"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/book1_introduction"
        android:padding="@dimen/small_spacing"/>
</androidx.core.widget.NestedScrollView>

这里我们使用NestedScrollView包含了一个TextView,而TextView的文本就是书籍的概要内容。这里为什么要使用NestedScrollView 而不是ScrollView呢?是因为NestedScrollView扩展ScrollView滚动时订阅和通知CoordinatorLayout的行为。所以换成ScrollView是没有任何作用。这也是小白前面要先介绍吉祥四宝的原因。除非自定义,否则这里只有固定搭配。就像优秀的人,总是那么与众不同。

                                   

                                                                   图3 主要成员

伴奏就位

在我们的CoordinatorLayout主唱就位后,我们还需要特定的伴奏:Android Studio开发实战这本书的出版信息等。通常我们显示在头部,这里我们就需要用到AppBarLayout和ToolBar。我们知道默认Activity都有ActionBar。但是对于头部定制,ActionBar并不能很好的处理。所以Google定义了一套可定制化的头部控件:

  1. ToolBar: 工具栏,定义标题,导航按钮以及其他操作菜单。
  2. CollapsingToolBarLayout: 头部可缩放布局容器,可以包含其他内容。
  3. AppBarLayout:整体头部布局容器。

具体代码如下:

<com.google.android.material.appbar.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:fitsSystemWindows="true">

    <com.google.android.material.appbar.CollapsingToolbarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:contentScrim="@color/colorPrimary">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:src="@mipmap/header"
            ></ImageView>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:layout_marginTop="@dimen/big_spacing">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/book1_name"
                android:padding="@dimen/small_spacing"
                android:gravity="center"
                android:textColor="@color/colorWhite"
                android:textSize="@dimen/big_title_size"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="作者:欧阳燊"
                android:paddingTop="@dimen/small_spacing"
                android:paddingBottom="@dimen/small_spacing"
                android:paddingLeft="@dimen/default_spacing"
                android:paddingRight="@dimen/default_spacing"
                android:textColor="@color/colorWhite" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="出版社:清华大学出版社"
                android:paddingTop="@dimen/small_spacing"
                android:paddingBottom="@dimen/small_spacing"
                android:paddingLeft="@dimen/default_spacing"
                android:paddingRight="@dimen/default_spacing"
                android:textColor="@color/colorWhite" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="ISBN:9787302512608"
                android:paddingTop="@dimen/small_spacing"
                android:paddingBottom="@dimen/small_spacing"
                android:paddingLeft="@dimen/default_spacing"
                android:paddingRight="@dimen/default_spacing"
                android:textColor="@color/colorWhite" />
        </LinearLayout>
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="?actionBarSize"
            app:navigationIcon="@drawable/ic_baseline_arrow_back_24"/>
    </com.google.android.material.appbar.CollapsingToolbarLayout>

</com.google.android.material.appbar.AppBarLayout>

加上头部后,效果如:

                               

                                                           图4 加上头部

但是实际上,到这一步我们还是没有使用到CoordinatorLayout的任何功能。用其他布局控件也能实现相同的功能。而且有一点可能比较奇怪,你会发现NestedScrollView跑到了AppBarLayout下面,被遮挡了一部分。

那么怎么处理这个问题呢?

合唱指挥

就像合唱团内协调主唱和伴奏需要一个指挥。在CoordinatorLayout内部协调NestedScrollView和AppBarLayout也需要一个桥梁。这个桥梁就是app:layout_behavior。app:layout_behavior作用于NestedScrollView,指定了特定的路径。NestedScrollView就可以通过这个特定的路径,从外部影响和改变AppBarLayout内部内容。所以这里我们需要指定AppBarLayout的ScrollingViewBehavior:

<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">

指定Behavior后,NestedScrollView就知道怎么和AppBarLayout,就像和AppBarLayout达成了协议,而且NestedScrollView不会被AppBarLayout遮挡:

                                    

                                                                 图5 建立联系

伴奏

当主唱,伴奏都就绪,指挥也就绪后,演出可以正式开始了。当指挥挥动指挥棒后,主唱开始吟唱。但是钢琴和小提琴伴奏没有起来?就像NestedScrollView滚动的时候,AppBarLayout没有任何反应。这是因为AppBarLayout虽然是一个伴奏团,但是我们没有定义接收到指挥信号后的反应行为。这个反应行为就是:app:layout_scrollFlags。

app:layout_scrollFlags指定了接收到指挥信号后钢琴和小提琴伴奏的反应:

  • noScroll: 不滚动,意思是你随意,爱咋咋的。

  • scroll: 跟着滚动,意思就是我跟着你一起动。

  • enterAlways: 不能单独使用,是scroll行为的细分操作,需要和scroll一起配置,并需要CollapsingToolBarLayout一起使用才能发挥效果。效果就是AppBarLayout的头部内容先滚进屏幕,然后才是NestedScrollView滚动。

  • enterAlwaysCollapsed: 是对enterAlways的进一步细分,需要和scroll以及enterAlways一起配置。 enterAlwaysCollapsed将AppBarLayout头部内容进一步分为扩展后的内容和缩放后的内容。就像上面实际例子里,AppBarLayout缩放后就仅显示ToolBar,而里面的图片和书籍出版信息需要等AppBarLayout完全展开时候才会显示。这时候就有三个滚动的反应对象:NestedScrollVIew的内容,ToolBar,和AppBarLayout其他需要展开显示的内容。而enterAlwaysCollapsed意味着滚动进入的顺序的变化:ToolBar -> NestedScrollView -> AppBarLayout其他内容。

  • exitUntilCollapsed: 从名字上也可以看,和enterAlways不同的是处理滚出屏幕的反应效果。和enterAlwaysCollapsed一样对AppBarLayout的滚动效果做了更细致的规划。exitUntilCollapsed意思是AppBarLayout其他内容先滚动出屏幕,然后才是NestedScrollView,接着才是ToolBar。

后面我们将以exitUntilCollapsed为例,app:layout_scrollFlags需要配置在AppBarLayout的子内容。**另外强调一点app:layout_scrollFlags只对ToolBar和CollapsingToolBarLayout有作用,其他控件并不会对其有任何反应。**例如我们直接把LinearLayout放到AppBarLayout内,并配置了app:layout_scrollFlags,LinearLayout就不会有任何反应。这里我们给CollapsingToolBarLayout设置app:layout_scrollFlags,代码如下:

<com.google.android.material.appbar.CollapsingToolbarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:contentScrim="@color/colorPrimary"
    app:layout_scrollFlags="scroll|exitUntilCollapsed">

时机

前面我们已经介绍过AppBarLayout,接下来我们了解一下CollapsingToolBarLayout。CollapsingToolBarLayout是可伸缩的工具栏布局容器。它的作用就是精细控制内部内容的伸缩特性。而具体怎么做呢?就是时机。就如同合唱团合唱时候需要伴奏,但是伴奏也不是随心所欲的,需要讲究点。什么时候伴奏该起来,什么时候伴奏应该停止,都是要讲究时机的。CollapsingToolBarLayout就是帮助我们设置这个时机:什么时候该展开,什么时候应该收起,更近一步什么时候什么应该展开等。

而控制时机的魔法棒就是app:layout_collapseMode和app:layout_collapseParallaxMultiplier。app:layout_collapseMode控制伸缩的模式:

  • pin: 悬停,意思是不参与伸缩。

  • parallax: 并行混合,意思是参与伸缩,并且可以和其他控件一起进行。

当我们选择parallax作为pp:layout_collapseMode时,我们就可以通过app:layout_collapseParallaxMultiplier进一步优化伸缩的时机。app:layout_collapseParallaxMultiplier是一个高度的比例值,它的意思就是告诉CollapsingToolBarLayout当伸缩比例到达高度的百分之几时候,才能展开或者收起内容。例如在例子中我们给CollapsingToolBarLayout的ImageView 和 LinearLayout同时设置上app:layout_collapseParallaxMultiplier=“0.7”,就意味着当收缩到只有最大高度的30%的时候,ImageView和LinearLayout就应该收起隐去或者展开显示,代码如下:

<ImageView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:src="@mipmap/header"
    app:layout_collapseMode="parallax"
    app:layout_collapseParallaxMultiplier="0.7"
    ></ImageView>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:layout_marginTop="@dimen/big_spacing"
    app:layout_collapseMode="parallax"
    app:layout_collapseParallaxMultiplier="0.7">

同时我们最好给CollapsingToolBarLayout设置app:contentScrim。app:contentScrim类似于一个兜底条款。如果没有自定义的图片背景,那么默认显示app:contentScrim。app:contentScrim可能是一个颜色,也可能是另外一个图片。在事例中,我设置了一个紫色的背景色,如下图:

                              

                              

                                                      图6 收缩前和收缩后

特别伴奏

除了通过AppBarLayout 和 CollapsingToolBarLayout 之外,我们还可以给我们的合唱团,添加打击乐伴奏。而这个特别伴奏需要用到app:layout_anchor和app:layout_anchorGravity,即传说中的锚。app:layout_anchor和app:layout_anchorGravity可以让特殊的控件例如FloatingActionButton

可以定位特定的控件并且伴随一起反应,有点类似RelativeLayout的作用,但是有多了一些新的特性。在事例中,我们构建一个FloatingActionButton的编辑按钮,将它锚定在AppBarLayout的右下角,代码如下:

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:srcCompat="@drawable/ic_baseline_add_24"
    android:layout_marginRight="@dimen/small_spacing"
    app:layout_anchor="@id/appbar"
    app:layout_anchorGravity="bottom|right"/>

最后

到这里我们基本就完成了CoordinatorLayout协调布局的介绍。说时候CoordinatorLayout布局特别难讲解,因为它不像LinearLayout和RelativeLayout那么直来直去。很多特效需要一套控件来把控实现。无论怎么样,我们都学到了一些新的知识不是么。付出总是有回报的,最后让我们来看一下成品吧。另外别忘了使用AppBarLayout和ToolBar,请务必在AndroidManifest.xml将App的theme改成某一种NoActionBar例如android:theme**=“@style/Theme.AppCompat.Light.NoActionBar”。**