本文介绍以下很 google的UI控件:CoordinatorLayout、AppbarLayout、NestedScrollView以及CollapsingToolbarLayout。它们共同实现了下面这样的效果:
- 初始控件
「CoordinatorLayout」:本质是一个「FrameLayout」,是一个“super-powered FrameLayout”,它的主要作用:作为顶层布局,协调子View之间的交互;
「AppBarLayout」:是一种支持响应滚动手势的app bar布局,与「CoordinatorLayout」控件一起使用,实现「AppBarLayout」内部子View的Material Design滚动效果;
「NestedScrollView」:若是需要「AppbarLayout」中的子View实现滚动效果,需要配合实现一个带有「Scroll」属性的View,这个View最好是「NestedScrollView」、「RecyclerView」或是存在「Scroll」属性的控件模块(本文是一个带有RecyclerView的Fragment模块);
「CollapsingToolbarLayout」:「CollapsingToolbarLayout」是一个ViewGroup,看名字知道,它主要是用来包装「Toolbar」控件,实现折叠(其实就是看起来像伸缩~)的效果,它需要作为「AppBarLayout」布局的直接子View。
- layout_scrollFlags和layout_behavior
使用「CoordinatorLayout」、「AppBarLayout」以及实现了「Scroll」属性的「Fragment」共同实现Toolbar的滚动显示和隐藏效果。 先上效果图以及代码:
main_activity.layout
<?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=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</android.support.design.widget.AppBarLayout>
<FrameLayout
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
fragment.layout
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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=".fragment.mvp.view.imp.MainView">
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/record_list_refresh_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabs">
<com.scwang.smartrefresh.layout.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:srlAccentColor="@color/white"
app:srlPrimaryColor="@color/red_pressed"/>
<com.ecg.rencarehealth.recordcollect.view.SwapMenuRecyclerView.SwapRecyclerView
android:id="@+id/record_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
</android.support.constraint.ConstraintLayout>
代码中用到的依赖库:
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation "com.android.support:design:$support_version"
implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.0.3'
上面的代码简单实现了Toolbar的滚动显示和滚动隐藏效果。在这里需要了解实现这样效果的两个关键属性:「app:layout_scrollFlags」、「app:layout_behavior」。
AppBarLayout 继承自LinearLayout,布局方向为垂直方向。所以你可以把它当成垂直布局的LinearLayout来使用。AppBarLayout是在LinearLayou上加了一些材料设计的概念,它可以让你定制当某个可滚动View的滚动手势发生变化时,其内部的子View实现何种动作。
「app:layout_behavior」:控件「ConstraintLayout」中实现「NestedScrollView」机制(Fragment模块中包含RecyclerView控件)的子View(不包括AppBarLayout),设置该属性的固定value:
app:layout_behavior="@string/appbar_scrolling_view_behavior"
这是一个系统behavior, 从字面意思就可以看到, 是为appbar设置滚动动作的一个behavior. 没有这个属性的话, Appbar就是死的, 有了它就有了灵魂.
我们可以通过给Appbar下的子View添加app:layout_scrollFlags来设置各子View执行的动作. scrollFlags可以设置的动作如下:
「app:layout_scrollFlags」:
-
scroll: 值设为scroll的View(Toolbar)会跟随滚动事件一起发生移动。即当带有「Scroll」属性的View滚动时,该View(Toolbar)也跟随一起滚动,就好像这个View也是属于这个ScrollView一样。
上面的效果图就是Toolbar设置了app:layout_scrollFlags="scroll"的效果图。 -
enterAlways: 顾名思义,View(Toolbar)总是(Always)“进入”。这个“进入”的意思,包括显示和隐藏;也即任何时候ScrollView滚动时,该View(Toolbar)都会同步滚动出现(隐藏)。
修改Toolbar的app:layout_scrollFlags:
app:layout_scrollFlags="scroll|enterAlways"
- exitUntilCollapsed:直译过来是“折叠之后退出”。当此View(Toolbar)要往上逐渐“隐藏”时,会一直往上滑动,直到折叠完成(剩下「最小高度」)后,再响应ScrollView的内部滑动事件。
修改代码,直接查看效果图可能更容易理解:
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:minHeight="16dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
- enterAlwaysCollapsed:看名字,结合了enterAlways与Collapsed(折叠)功能。当View(Toolbar)滚动“出现”时,首先是enterAlways效果,当View(Toolbar)显示出来的部分达到最小高度时,View就暂时停止滚动;ScrollView继续滑动到顶部时,View(Toolbar)再继续往下滑动,直到滑到View(Toolbar)的顶部结束。
这个效果,需要设置View(Toolbar)的最大和最小高度(Toolbar默认的最小高度就是?attr/actionBarSize),直接上代码和效果图,理解这个效果:
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="188dp"
android:minHeight="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
注意:从图上可以看出, 在最大高度时,title的位置和toolbar上的图标行脱离了;并且View(Toolbar)滚动出现到最小高度时,title和图标都没有出现。 即使在布局里添加了 android:gravity=“bottom|start”, 在toolbar滚动的时候, title还在, 图标依然滚动到隐藏了,这个问题在后边的「CollapsingToolbarLayout」控件中得以解决。
- snap:View(Toolbar)带有“吸附”效果。即View(Toolbar)不会存在局部显示的情况:要不完全隐藏,要不完全显示,有点类似ViewPager的左右滑动。
上代码和效果图:
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="100dp"
app:layout_scrollFlags="scroll|snap"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
- 「CollapsingToolbarLayout」控件
直译名称:“折叠Toolbar的布局”,根据名称理解控件的功能。理解的意思是对Toolbar进行包装的ViewGroup,实现Toolbar的折叠(其实就是看起来像伸缩~)的效果。它需要放在AppBarLayout布局里面,并且作为AppBarLayout的直接子View。主要的功能介绍如下:
-
(1) 折叠Title(Collapsing title):布局完全显示时,Toolbar的Title是最大的;但随着View(Toolbar)逐步折叠滚动隐藏,title逐渐减小并最终显示在Toolbar正常大小情况下的Title位置。
-
(2)内容纱布(Content scrim):根据滚动的位置(这个位置系统内定),决定是否对View(Toolbar)“盖上纱布”。可以通过setContentScrim(Drawable)来设置纱布的图片. 默认contentScrim是colorPrimary的色值。
-
(3)状态栏纱布(Status bar scrim):根据滚动的位置(这个位置系统内定),决定是否对状态栏“盖上纱布”,可以通过setStatusBarScrim(Drawable)来设置纱布图片,但是只能在LOLLIPOP设备上面有作用。默认statusBarScrim是colorPrimaryDark的色值。
-
(4)视差滚动子View(Parallax scrolling children): View(CollapsingToolbarLayout中可以有多个子View,此View不限于Toolbar)在当前的布局中按照“视差”的方式来跟随滚动。(PS:其实就是让这个View的滚动的速度比其他正常滚动的View速度稍微慢一点)。将布局参数app:layout_collapseMode设为parallax
-
(5)子View位置固定(Pinned position children):子View可以选择是否在全局空间上固定位置,这对于Toolbar来说非常有用,因为当布局在移动时,可以将Toolbar固定位置而不受移动的影响。 将app:layout_collapseMode设为pin。
按照上面对于控件「CollapsingToolbarLayout」的功能描述,我们对代码中「AppBarLayout」控件及其子View进行修改,并查看效果图:
<android.support.design.widget.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_210"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
app:contentScrim="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_collapseMode="pin"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
可以看到,我们使用「CollapsingToolbarLayout」对「Toolbar」控件进行包装,并将一些原属于「AppBarLayout」布局内部的属性提取到「CollapsingToolbarLayout」控件中:
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
app:popupTheme="@style/AppTheme.PopupOverlay"
同时,设置「CollapsingToolbarLayout」的“内容纱布”的代码为:
app:contentScrim="?attr/colorPrimary"
设置「Toolbar」的位置固定:
app:layout_collapseMode="pin"
效果如图:
基本达到了文章开头的效果图样式,现在还欠缺的就是「Toolbar」最大化显示时的背景图片、背景图片的折叠效果、内容(Toolbar)栏以及状态栏的UI变化效果。
上面既然介绍到控件「CollapsingToolbarLayout」是一个ViewGroup,那么它就可以再容纳其他的子View,此时就可以添加我们需要的背景图片了。
看代码:
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
app:contentScrim="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@drawable/test_image_view"
app:layout_collapseMode="parallax"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_collapseMode="pin"/>
</android.support.design.widget.CollapsingToolbarLayout>
代码中在控件「CollapsingToolbarLayout」的布局中,增加一个ImageView类型的子View,并添加如下「CollapsingToolbarLayout」的功能代码
app:layout_collapseMode="parallax"
再看效果图:
注意:代码中设置的没有设置「Toolbar」的背景,只是在「CollapsingToolbarLayout」布局中增加了“内容纱布”背景代码。所以在「Toolbar」完全显示时的背景栏为透明的,达到最小高度时的背景为「ColorPrimary」
这节最后再说一下,因为目前大部分的app设计都是沉浸式的状态栏,即状态栏透明,与「Toolbar」的背景一样。所以我们可以将状态栏的背景设置为透明,这就需要修改主题了:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
...
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
在布局里面, 将ImageView和所有它上面的父View都添加fitsSystemWindows属性
<?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"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_210"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
app:contentScrim="?attr/colorPrimary"
android:fitsSystemWindows="true"
app:popupTheme="@style/AppTheme.PopupOverlay">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@drawable/test_image_view"
app:layout_collapseMode="parallax"/>
...
效果图:
- 「FloatingActionButton」
作为Google Material Design的一个重要控件,「FloatingActionButton」怎么可能不在「AppBarLayout」中起点作用。就像文章开头那里的效果一样, 我们在布局中加一个悬浮按钮,并让它“锚定”在「AppBarLayout」控件的右下角. 这样这个悬浮按钮就和「AppBarLayout」关联起来了。
还是看代码,看效果图:
<?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"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_210"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
app:contentScrim="?attr/colorPrimary"
android:fitsSystemWindows="true"
app:popupTheme="@style/AppTheme.PopupOverlay">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@drawable/test_image_view"
app:layout_collapseMode="parallax"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_collapseMode="pin"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<FrameLayout
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/record_collect_main_fragment_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/dimen_8"
app:rippleColor="@color/red_pressed"
app:layout_anchor="@id/appBarLayout"
app:layout_anchorGravity="bottom|end"
app:srcCompat="@android:drawable/ic_input_add"/>
</android.support.design.widget.CoordinatorLayout>
效果图:
最后的最后,文章开头的效果图中,还有一个「TabLayout」控件的效果。这个就直接在「Fragment」对应的布局文件中增加控件,并在代码中添加对应的「TabLayout.Tab」就可以了。
上代码:
fragment.layout
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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=".fragment.mvp.view.imp.MainView">
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/red_normal"
app:tabTextColor="@android:color/darker_gray"
app:tabSelectedTextColor="@color/white"/>
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/record_list_refresh_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabs">
<com.scwang.smartrefresh.layout.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:srlAccentColor="@color/white"
app:srlPrimaryColor="@color/red_pressed"/>
<com.ecg.rencarehealth.recordcollect.view.SwapMenuRecyclerView.SwapRecyclerView
android:id="@+id/record_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
</android.support.constraint.ConstraintLayout>
java代码中增加「TabLayout.Tab」:
...
val tabLayout = view.findViewById<TabLayout>(R.id.tabs)
tabLayout.addTab(tabLayout.newTab().setText("标题一"))
tabLayout.addTab(tabLayout.newTab().setText("标题二"))
tabLayout.addTab(tabLayout.newTab().setText("标题三"))
tabLayout.addTab(tabLayout.newTab().setText("标题四"))
val defaultTab = tabLayout.newTab().setText("标题五")
tabLayout.addTab(defaultTab)
defaultTab.select()
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
var pos = tab.position
}
override fun onTabUnselected(tab: TabLayout.Tab) {
var pos = tab.position
}
override fun onTabReselected(tab: TabLayout.Tab) {
var pos = tab.position
}
})
...
最终的效果图,就是文章开头的样子了。
OK,到这里终于搞定这个比较Material Design的Toolbar效果了。本文参考了如下文章: