CoordinatorLayout的基本使用

CoordinatorLayout简介

       CoordinatorLayout是在Google IO/15 大会发布的,遵循Material风格,包含在support Library中,结合AppbarLayout、CollapsingToolbarLayout等可产生各种炫酷的效果。

       CoordinatorLayout是加强版的FrameLayout,但并不是直接继承,基本的布局方式跟FrameLayout是一样的。主要有两个用途:

  • 用作应用的顶层布局管理器,也就是作为用户界面中所有UI控件的容器;
  • 用作相互之间具有特定交互行为的UI控件的容器,通过为CoordinatorLayout的子View指定Behavior,就可以实现它们之间的交互行为。 Behavior可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单,可滑动删除的UI元素,以及跟随着其它UI控件移动的按钮等。

与child之间的互动

       CoordinatorLayout主要提供了三种方式来实现child之间的互动:anchor、insetEdge、Behaviors

anchor

       关键属性:layout_anchor和layout_anchorGravity,child B通过layout_anchor设置child A为anchor,再通过layout_anchorGravity来根据需要设置属性,这样B就可以以A的位移相应的位移了。代码如下:

       先设置一个被观察的child A的id:

1
2
3
4
5
6
<com.wy521angel.coordinatorlayouttest.TouchView
android:id="@+id/view_host"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center"
android:background="@color/colorPrimary" />

       然后在另一个观察的child B的设置两个参数,layout_anchor和layout_anchorGravity:

1
2
3
4
5
6
<View
android:layout_width="80dp"
android:layout_height="80dp"
android:background="@color/colorAccent"
app:layout_anchor="@id/view_host"
app:layout_anchorGravity="bottom|end" />

       B就会随着A的移动而跟着移动,效果如下图:

CoordinatorLayout与child之间互动图

insetEdge

       child A通过layout_insetEdge来设置插入CoordinatorLayout的方向,child B通过设置layout_dodgeInsetEdges来躲避来自相同方向的A,这样就可以避免产生重叠。

       先在被观察的child A中设置参数layout_insetEdge:

1
2
3
4
5
6
7
<com.wy521angel.coordinatorlayouttest.TouchView
android:id="@+id/view_host"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center"
android:background="@color/colorPrimary"
app:layout_insetEdge="top" />

       在观察的child B中设置参数layout_dodgeInsetEdges:

1
2
3
4
5
6
<View
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="20dp"
android:background="@android:color/black"
app:layout_dodgeInsetEdges="top" />

       效果如CoordinatorLayout与child之间互动图中的黑块。

Behaviors

       详见CoordinatorLayout之Behavior

与其它控件联合使用

FloatingActionButton

       FloatingActionButton单独使用,布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
...>

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:onClick="onClick" />

</FrameLayout>

       点击按钮弹出一个Snackbar

1
2
3
4
@Override
public void onClick(View v) {
Snackbar.make(findViewById(R.id.contentView), "Snackbar", Snackbar.LENGTH_SHORT).show();
}

       效果如下:

       按钮会被Snackbar遮挡,此时需要使用到CoordinatorLayout,与CoordinatorLayout一起使用,布局调整如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
...>

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:onClick="onClick" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

       原因是FloatingActionButton自带app:layout_dodgeInsetEdges=”bottom”,
而Snackbar自带app:layout_insetEdge=”bottom”,所以当Snackbar出现的时候FloatingActionButton能发生躲避行为。

       我们还可以自己加一个View来验证一下Snackbar是自带app:layout_insetEdge=”bottom”

1
2
3
4
5
6
<View
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="bottom"
android:background="@android:color/holo_blue_bright"
app:layout_dodgeInsetEdges="bottom" />

       这个view加入了app:layout_dodgeInsetEdges=”bottom就可以产生和FloatingActionButton一样的行为,效果如下:

AppBarLayout

       AppBarLayout继承自LinearLayout,基本布局方式跟LinearLayout是一样的。AppBarLayout必须作为CoordinatorLayout的直接子View,否则它的大部分功能将不会生效,如layout_scrollFlags等。

       看如下效果图:

AppBarLayout基本效果图

       上面这个效果在不少APP中都很常见,标题栏会随着视图的滚动显示和隐藏。

       不使用CoordinatorLayout,实现这个效果的方案有两种:

  1. 自己处理触摸事件的分发来改变标题栏的位置。
  2. 使用NestedScrolling机制。

       NestedScrolling两个重要的概念:

  • NestedScrollingParent
  • NestedScrollingChild

       CoordinatorLayout已经实现了NestedScrollingParent接口,所以配合一个实现了NestedScrollingChild接口的View就可以轻松的实现以上效果,类似如下控件:NavigationMenuView、NestedScrollView、RecyclerView、SwipeRefreshLayout等等。最常用的应该是NestedScrollView和RecyclerView,注意ScrollView是不可以的。

       简单起见,我们使用NestedScrollView包裹TextView来实现上面的效果,主要布局代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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">

<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="标题"
android:textColor="@android:color/white"
android:textSize="20sp"
app:layout_scrollFlags="scroll" />

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

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

<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:textSize="30sp" />

</androidx.core.widget.NestedScrollView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

       CoordinatorLayout中可滚动的视图(如本例中的NestedScrollView),需要设置以下属性:

1
app:layout_behavior="@string/appbar_scrolling_view_behavior"

       这个固定字符串是系统提供的,表示使用android.support.design.widget.AppBarLayout$ScrollingViewBehavior来处理NestedScrollView与AppBarLayout的关系。

       使用AppBarLayout的关键点是,在Child里面设置属性layout_scrollFlags,layout_scrollFlags可以设置如下5个参数:

  • scroll
  • snap
  • enterAlways
  • enterAlwaysCollapsed
  • exitUntilCollapsed

scroll

       这参数是其它四个参数的基础,即只有设置了这个参数,其它参数才会生效。相应的Child才会对滚动view的滚动响应。

1
app:layout_scrollFlags="scroll"

       scroll隐藏的时候,先整体向上滚动,直到AppBarLayout完全隐藏,再开始滚动Scrolling View;显示的时候,直到Scrolling View顶部完全出现后,再开始滚动AppBarLayout到完全显示,如AppBarLayout基本效果图所示。

       除了scroll,其它几个参数都必须与scroll一起使用 “|” 运算符。

snap

       这个参数的作用是让Child具有吸附效果,抬手后会根据距离向上或向下滑动。

1
app:layout_scrollFlags="scroll|snap"

enterAlways

       enterAlways与scroll类似,只不过向下滚动先显示AppBarLayout到完全,再滚动 Scrolling View。

1
app:layout_scrollFlags="scroll|enterAlways"

       效果如下:

enterAlwaysCollapsed

       这个参数是enterAlways的附加值,需要和enterAlways一起使用,和enterAlways不一样的是,不会显示AppBarLayout到完全再滚动Scrolling View,而是先滚动AppBarLayout到最小高度,再滚动Scrolling View,最后再滚动AppBarLayout到完全显示。此外还需要定义View的最小高度才有效果:

1
2
android:minHeight="10dp"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"

       效果如下:

exitUntilCollapsed

       定义了AppBarLayout消失的规则。发生向上滚动事件时,AppBarLayout向上滚动退出直至最小高度(minHeight),然后Scrolling View开始滚动。也就是AppBarLayout不会完全退出屏幕。

1
2
android:minHeight="10dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed"

       效果如下:

       enterAlwaysCollapsed与exitUntilCollapsed在实际的使用中,更多的是与CollapsingToolbarLayout一起使用,继续看下一节。

CollapsingToolbarLayout

       CollapsingToolbarLayout继承自FrameLayout,所以布局特性和FrameLayout一样。简单来说,它是工具栏的包装器,是带有视差动效的toolbar,是放在AppBarLayout中的一个直接子View。主要实现以下功能:

  • Collapsing title(可以折叠的标题 )
  • Content scrim(内容装饰),当我们滑动的位置到达一定阀值的时候,内容装饰将会被显示或者隐藏;
  • Status bar scrim(状态栏布)
  • Parallax scrolling children,滑动的时候子View呈现视觉特差效果
  • Pinned position children,固定位置的子View

       看如下基本布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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">

<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="150dp">

<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="@android:color/holo_orange_light"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@android:color/holo_green_light"
android:minHeight="10dp"
app:layout_collapseMode="pin" />

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


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

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

<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:textSize="30sp" />

</androidx.core.widget.NestedScrollView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

       结合enterAlwaysCollapsed使用,效果如下:

       修改下CollapsingToolbarLayout的layout_scrollFlags:

1
app:layout_scrollFlags="scroll|exitUntilCollapsed"

       效果如下:

       下面简单介绍一下CollapsingToolbarLayout的几个属性:

  • collapsedTitleTextAppearance,expandedTitleTextAppearance
    这两个可以定制标题收起和展开的字体样式

  • contentScrim
    参数可以是color和drawable,简单来说就是收起状态Toolbar的背景

  • layout_collapseMode
    说明CollapsingToolbarLayout的child可以设置为两种模式,parallax和pin

    • pin
      简单的固定模式

    • parallax
      表示视差模式,即移动过程中两个View的位置产生了一定的视觉差异。可以根据需要搭配layout_collapseParallaxMultiplier,layout_collapseParallaxMultiplier的数值是0~1。0表示滚动没有视差,完全跟着下面的滚动view,1表示不动

       如下两个效果图,设置为0时,背景图片是随着滚动视图滚动的,设置为1时,则不随着滚动。

       此外,不要在toolbar运行时手动添加view到toolbar里面。

ViewPager

       布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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">

<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

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

<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/bg2"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.1" />

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" >

...

</androidx.appcompat.widget.Toolbar>

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

<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="?attr/colorPrimary"
app:tabIndicatorColor="@color/colorAccent"
app:tabIndicatorHeight="4dp"
app:tabSelectedTextColor="#000"
app:tabTextColor="#fff" />

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

<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

       TabLayout在滑动的时候最终会停靠在最顶部,是因为没有设置其layout_scrollFlags,即TabLayout是静态的。

       代码详见CoordinatorLayoutTest

参考资料:
Xugter CoordinatorLayout系列(一):基本使用
zhuhf CoordinatorLayout 完全解析
gdutxiaoxu 使用CoordinatorLayout打造各种炫酷的效果

Fork me on GitHub