Material Design是由Google的设计工程师们基于传统优秀的设计原则,结合丰富的创意和科学技术所开发的一套全新的界面设计语言,包含了视觉、运动、互动效果等特性。为了做出表率,Google从Android 5.0系统开始,就将所有内置的应用都使用Material Design风格进行设计。
一、Toolbar
Toolbar由AndroidX库提供,对于它的另一个相关控件ActionBar由于设计的原因,被限定只能位于Activity的顶部,从而不能实现Material Design的效果,因此官方现在已经不再建议使用ActionBar了。ToolBar强大之处在于不仅继承了ActionBar的所有功能,而且灵活性很高,可以配合其他控件完成一些Material Design效果,下面就来学习一下。
因为我们现在准备使用ToolBar替换ActionBar,所以需要指定一个不带ActionBar的主题,通常有以下两种主题可选:
- 深色主题
Theme.AppCompat.NoActionBar - 浅色主题
Theme.AppCompat.Light.NoActionBar
1.修改value->theme.xml中默认主题
<resources>
<style
name="Theme.Test"
parent="Theme.AppCompat.Light.NoActionBar">
</style>
</resources>
style中各个颜色的属性如下图所示:
2.使用ToolBar替代ActionBar
修改activity_main.xml代码:
<FrameLayout 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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</FrameLayout>
因为之前使用了浅色主题,所以ToolBar上面的元素就会自动使用深色系,上面的字都会变成黑色的会很难看。所以为了让ToolBar单独使用深色主题,这里使用android:theme属性将ToolBar主题指定成了深色主题,但是这样又会产生新的问题,就是弹出的菜单项也会变成深色主题,这样就再次变得十分难看了,于是这里又使用app:popupTheme属性单独将弹出的菜单项指定成浅色主题。
接下来修改MainActivity代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(...) {
...
// 将ToolBar实例传入
setSupportActionBar(toolbar)
}
}
3.修改标题栏显示文字内容
修改AndroidManifest.xml内容:
<application ...>
<activity
android:name=".MainActivity"
<!-- 设置标题 -->
android:label="Fruits">
</application>
4.给ToolBar添加action按钮
首先在res文件夹下新建menu文件夹,然后在menu文件夹下新建toolbar.xml文件并添加三个按钮:
<menu ...>
<item
android:id="@+id/backup"
android:icon="@drawable/ic_backup"
android:title="Backup"
app:showAsAction="always" />
<item
android:id="@+id/delete"
android:icon="@drawable/ic_delete"
android:title="Delete"
app:showAsAction="ifRoom" />
<item
android:id="@+id/settings"
android:icon="@drawable/ic_settings"
android:title="Settings"
app:showAsAction="never" />
</menu>
app:showAsAction用来指定按钮的显示位置,主要有以下几种值可选:
- always
永远显示在ToolBar中,如果屏幕空间不够则不显示。 - ifRoom
屏幕空间足够的情况下显示在ToolBar中,不够的话就显示在菜单当中当中。 - never
永远显示在菜单当中。
注意:Toolbar中的action按钮只会显示图标,菜单中的action按钮只会显示文字。
5.给Action按钮添加点击事件
修改MainActivity代码:
class MainActivity : AppCompatActivity() {
...
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// 加载toolbar.xml菜单文件
menuInflater.inflate(R.menu.toolbar, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// 处理各个按钮点击事件
when(item.itemId) {
R.id.backup -> ...
R.id.delete -> ...
R.id.settings -> ...
}
return true
}
}
最终展示ToolBar效果图如下所示:
二、滑动菜单——DrawerLayout
所谓滑动菜单,就是将一些菜单选项隐藏起来,而不是放置在主屏幕上,然后通过滑动的方式将菜单显示出来。既节省了屏幕空间,又实现了非常好的动画效果,是Material Design中推荐的做法。
DrawerLayout是一个布局,在布局中允许放入两个直接子控件:第一个子控件是主屏幕中显示的内容,第二个子控件是滑动菜单中显示的内容。因此我们可以对activity_main.xml中的代码做如下修改:
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- FrameLayout中组件作为主屏幕中显示内容 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
/>
</FrameLayout>
<!-- 作为滑动菜单中显示的内容 -->
<!-- android:layout_gravity必须指定,用于告诉DrawerLayout是在屏幕左边还是右边 -->
<!-- start表示根据系统语言方向进行判断 -->
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#FFF"
android:text="This is menu"
android:textSize="30sp" />
</androidx.drawerlayout.widget.DrawerLayout>
呈现效果如下图所示:
1.在Toolbar最左侧加入菜单按钮
class MainActivity : AppCompatActivity() {
override fun onCreate(...) {
...
// 获得ActionBar实例
supportActionBar?.let {
// 让按钮显示出来
it.setDisplayHomeAsUpEnabled(true)
// 设置按钮图标
it.setHomeAsUpIndicator(R.drawable.ic_menu)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
// 点击后调用openDrawer()方法将滑动菜单显示出来
android.R.id.home ->
drawerLayout.openDrawer(GravityCompat.START)
}
}
}
最终呈现效果如下所示:
2.NavigationView
我们可以在滑动菜单中定制任意的布局,不过Google给我们提供了一种更好的方法——使用NavigationView。它是Material库中提供的一个控件,它不仅是严格按照Material Design的要求来设计的,而且可以将滑动菜单页面的实现变得非常简单。
使用NavigationView可以轻松实现下图的滑动菜单效果:
首先,既然这个控件是Material提供的,就需要将这个库引入项目中才行:
dependencies {
// Material库
implementation 'com.google.android.material:material:1.1.0'
// 开源项目,CircleImageView,轻松实现图片圆形化功能
implementation 'de.hdodenhof:circleimageview:3.0.1'
}
2.1.准备menu
menu是用来在NavigationView中显示具体的菜单项的。
在res中新建menu文件夹,然后右键menu文件夹->New->Menu resource file,创建一个nav_menu.xml文件:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<!-- checkableBehavior="single"表示所有选项只能单选 -->
<group android:checkableBehavior="single">
<item
android:id="@+id/navCall"
android:icon="@drawable/nav_call"
android:title="Call" />
<item
android:id="@+id/navFriends"
android:icon="@drawable/nav_friends"
android:title="Friends" />
<item
android:id="@+id/navLocation"
android:icon="@drawable/nav_location"
android:title="Location" />
<item
android:id="@+id/navMail"
android:icon="@drawable/nav_mail"
android:title="Mail" />
<item
android:id="@+id/navTask"
android:icon="@drawable/nav_Task"
android:title="Tasks" />
</group>
</menu>
2.2.准备headerLayout
headerLayout是用来在NavigationView中显示头部布局的。
这是一个可以随意定制的布局,我们就在headerLayout中放置头像、用户名、邮箱地址这3项。
在layout文件夹下新建nav_header.xml文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
<!-- 180dp是NavigationView比较合适的高度 -->
android:layout_height="180dp"
android:padding="10dp"
android:background="@color/colorPrimary">
<!-- 显示的图片是圆形 -->
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/iconImage"
android:layout_width="70dp"
android:layout_height="70dp"
android:src="@drawable/nav_icon"
android:layout_centerInParent="true" />
<TextView
android:id="@+id/mailText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="tonygreendev@gmail.com"
android:textColor="#FFF"
android:textSiz="14sp" />
<TextView
android:id="@+id/userText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:above="@id/mailText"
android:text="Tony Green"
android:textColor="#FFF"
android:textSizr="14sp" />
</RelativeLayout>
2.3.修改activity_main.xml
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
<!-- 设置刚才准备的menu -->
app:menu="@menu/nav_menu"
<!-- 设置刚才准备的headerLayout -->
app:headerLayout="@layout/nav_header" />
</androidx.drawerlayout.widget.DrawerLayout>
2.4.设置菜单项点击事件
class MainActivity : AppCompatActivity() {
override fun onCreate(...) {
...
// 将Call项设为默认选中
navView.setCheckedItem(R.id.navCall)
// 设置菜单项选中事件监听器
navView.setNavigationItemSelectedListener {
// 将滑动菜单关闭
drawerLayout.closeDrawers()
// true表示此事件已被处理
true
}
}
}
三、悬浮按钮和可交互提示
1.悬浮按钮——FloatingActionButton
FloatingActionButton是Material库中提供的一个控件,这个控件可以帮助我们比较轻松实现悬浮按钮的效果,它默认使用colorAccent作为按钮的颜色,我们还可以通过一个图标来表示这个按钮的作用是什么。
修改activity_main.xml:
<androidx.drawerlayout.widget.DrawerLayout ... >
<FrameLayout ...>
...
<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_margin="16dp"
android:src="@drawable/ic_done"
<!-- 指定悬浮高度/阴影大小 -->
app:elevation="8dp" />
</FrameLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
按钮的点击事件和普通按钮一致:
fab.setOnClickListener {
// 处理点击事件
}
2.可交互提示——Snackbar
Snackbar并不是Toast的替代品,它们有着不同的应用场景,Toast作用是告诉用户现在发生了什么事,但是用户只能被动接受这个事情。而Snackbar则在这方面进行了扩展,它允许在提示中加入一个可交互按钮,当用户点击按钮的时候,可以执行一些额外的操作。
修改MainActivity代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(...) {
...
fab.setOnClickListener { view ->
// 传入任意布局view即可
Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT)
.setAction("Undo") {
// 处理Snackbar上面的按钮点击事件
}
.show()
}
}
}
这里还会存在一个问题,Snackbar显示的时候可能会把FloatingActionButton遮挡住,想要解决这个问题,就要引入下一个知识点了。
四、提供Material的布局——CoordinatorLayout
CoordinatorLayout可以说是一个加强版的FrameLayout,由AndroidX库提供,它在普通情况下的作用和FrameLayout基本一直,但是它拥有一些额外的Material能力。
事实上,CoordinatorLayout可以监听其所有的子控件的各种事情,并自动帮我们做出最为合理的响应。比如说,刚才弹出的Snackbar会将悬浮按钮挡住,而如果我们能让CoordinatorLayout监听到Snackbar的弹出事件,那么它会自动将内部的FloatingActionButton向上偏移,从而确保不会被Snackbar遮挡。
CoordinatorLayout的使用也非常简单,只需要将原来的FrameLayout替换一下就可以了,现在修改activity_main.xml:
<androidx.drawerlayout.widget.DrawerLayout ...>
<!-- 替换原来的FrameLayout -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar ...>
<com.google.android.material.floatingactionbutton.FloatingActionButton ...>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.drawerlayout.widget.DrawerLayout>
由于CoordinatorLayout是一个加强版的FrameLayout,因此这种替换不会有任何的副作用。现在再次触发Snackbar显示,悬浮按钮就会向上偏移了Snackbar的同等高度,从而确保不会被完全遮挡,当Snackbar消失的时候,悬浮按钮会自动向下偏移到原来的位置。
五、卡片式布局
这节中我们学习如何实现卡片式布局的效果。卡片式布局也是Materials Design中提出的一个新概念,它可以让页面中的元素看起来就像在卡片中一样,并且还能拥有圆角和投影,类似下图:
1.MaterialCardView基本用法
MaterialCardView是用于实现卡片式布局的重要控件,由Material库提供。实际上MatericalCardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉。
先来看一下MaterialCardView的基本用法:
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
<!-- 指定卡片圆角弧度 -->
app:cardCornerRadius="4dp"
<!-- 指定卡片投影高度/阴影大小 -->
app:elevation="5dp">
<!-- 在MaterialCardView中放置一个TextView -->
<!-- 这个TextView就会显示在一张卡片中 -->
<TextView
android:id="@+id/infoText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.card.MaterialCardView>
2.利用RecyclerView实现图片中效果
首先要载入RecyclerView依赖:
dependencies {
implementation "androidx.recyclerview:recyclerview:1.1.0"
// Glide开源库,用于加载图片到ImageView中等
// 可以加载网络图片、GIF、甚至是本地视频
implementation "com.github.bumptech:glide:glide:4.9.0"
}
2.1.修改activity_main.xml代码
<androidx.drawerlayout.widget.DrawerLayout ...>
<androidx.coordinatorlayout.widget.CoordinatorLayout ...>
...
<!-- 在CoordinatorLayout中添加RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
2.2.定义实体类Fruit
class Fruit(val name: String, val imageId: Int)
2.3.给RecyclerView指定自定义布局
在layout目录下新建fruit_item.xml:
<com.google.android.material.card.MaterialCardView
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="wrap_content"
android:layout_margin="5dp"
app:cardCornerRadius="4dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 用于显示水果图片 -->
<ImageView
android:id="@+id/fruitImage"
android:layout_width="wrap_content"
android:layout_height="100dp"
<!-- 指定图片缩放模式 -->
<!-- centerCrop让图片保持原有比例填充满ImageView -->
<!-- 超出的部分裁切掉 -->
android:scaleType="centerCrop"
/>
<!-- 用于显示水果名字 -->
<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="5dp"
android:textSize="16sp"
/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
2.4.给RecyclerView准备适配器
class FruitAdapter(val context: Context, val fruitList: List<Fruit>) :
RecyclerView.Adapter<FruitAdapter.ViewHolder>(){
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitName.text = fruit.name
// with()传入Context、Activity或者Fragment参数
// 调用load()加载图片,可以是URL、本地路径或是资源id
// 最后调用into()将图片设置到具体某一个ImageView中
Glide.with(context).load(fruit.imageId).into(holder.fruitImage)
}
override fun getItemCount() = fruitList.size
}
2.5.修改MainActivity代码
class MainActivity : AppCompatActivity() {
private val fruits = mutableListOf(Fruit("Apple", R.drawable.apple), Fruit("Banana", R.drawable.banana),
Fruit("Orange", R.drawable.orange), Fruit("Watermelon", R.drawable.watermelon),
Fruit("Pear", R.drawable.pear), Fruit("Grape", R.drawable.grape),
Fruit("Pineapple", R.drawable.pineapple), Fruit("Strawberry", R.drawable.strawberry),
Fruit("Cherry", R.drawable.cherry), Fruit("Mango", R.drawable.mango)
)
val fruitList = ArrayList<Fruit>()
override fun onCreate(...) {
...
initFruits()
// 网格布局,传入Context和列数
val layoutManager = GridLayoutManager(this, 2)
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(this, fruitList)
recyclerView.adapter = adapter
}
// 随机向fruitList中添加50个Fruit实例
private fun initFruits() {
fruitList.clear()
repeat(50) {
val index = (0 until fruits.size).random()
fruitList.add(fruits[index])
}
}
...
}
现在运行软件就可以看到类似于一开始的图片中的卡片布局的效果,但是有一点不一样的是Toolbar被RecyclerView给挡住了,如果想要解决这个问题,就需要引入下面一个知识点——AppBarLayout。
六、AppBarLayout
首先来分析一下为什么RecyclerView会把Toolbar给挡住呢,由于RecyclerView和Toolbar都是放在CoordinatorLayout中的,前面已近说过,CoordinatorLayout就是一个加强版的FrameLayout,FrameLayout中的所有控件在不进行明确定位的情况下,默认都会摆放在布局的左上角,从而产生遮挡现象。
AppBarLayout也是Material库中提供的另外一个工具,它实际上是一个垂直方向的LinearLayout,它在内部做了很多滚动事件的封装,并应用了一些Material Design的设计理念。
那么我们要怎样使用AppBarLayout来解决前面的遮挡问题呢,只需两步即可,第一步将Toolbar嵌套到AppBarLayout中,第二步给RecyclerView指定一个布局文件,修改activity_main.xml代码:
<androidx.drawerlayout.widget.DrawerLayout ...>
<androidx.coordinatorlayout.widget.CoordinatorLayout ...>
<!-- 将Toolbar嵌套在AppBarLayout中 -->
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar ...>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
<!-- 指定RecyclerView布局行为 -->
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
现在再次运行程序,可以发现一切都正常了!下面来进一步优化,实现一些Material Design效果。
当AppBarLayout接收到滚动事件的时候,它内部的子控件其实是可以指定如何去响应这些事件的,通过app:layout_scrollFlags属性就可以实现,修改activity_main.xml中的代码:
...
<androidx.appcompat.widget.Toolbar
...
<!-- scroll表示RecyclerView向上滚动时Toolbar一起向上滚动并隐藏 -->
<!-- enterAlways表示向下滚动时Toolbar会一起向下滚动并重新显示 -->
<!-- snap表示Toolbar还没完全隐藏或者显示时,根据当前滚动距离—— -->
<!-- ——自动选择隐藏还是显示 -->
app:layout_scrollFlags="scroll|enterAlways|snap" />
...
现在重新运行程序并向上滚动RecyclerView可以看到非常美观的Material Design效果!
七、下拉刷新——SwipeRefreshLayout
SwipeRefreshLayout是用于实现下拉刷新功能的核心类,它是由Androidx库提供,我们只需要把想要实现下拉刷新功能的控件放置到SwipeRefreshLayout中,就可以迅速让这个控件支持下拉刷新。在上面的项目中,应该支持下拉刷新的自然就是RecyclerView了。
1.修改activity_main.xml代码
...
<!-- 使用SwipeRefreshLayout包住RecyclerView -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
<!-- 也要添加布局行为声明 -->
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView .../>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
2.实现刷新逻辑
修改MainActivity代码:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(...) {
...
// 设置下拉刷新进度条颜色
swipeRefresh.setColorSchemeResources(R.color.colorPrimary)
// 设置下拉刷新监听器
swipeRefresh.setOnRefreshListener {
// 刷新RecyclerView内容
refreshFruits(adapter)
}
}
private fun refreshFruits(adapter: FruitAdapter) {
thread {
// 为了不让下拉刷新的进度条一下就消失,这里阻塞线程2秒
Thread.sleep(2000)
runOnUiThread {
// 随机给RecyclerView设置50个Fruit实例
initFruits()
// 通知RecyclerView的adapter数据发生变化
adapter.notifyDataSetChanged()
// 传入false表示刷新事件结束,并隐藏进度条
swipeRefresh.isRefreshing = false
}
}
}
}
八、可折叠式标题栏
虽说我们现在的标题栏是用Toolbar来编写的,不过它看上去和传统的ActionBar没有什么两样,只不过可以响应RecyclerView的滚动事件来进行隐藏和显示。而Material Design并没有限定标题栏必须是长这个样子的,事实上,我们可以根据自己的喜好随意定制标题栏的样式,那么本节就来实现一个可折叠式的标题栏效果,同时也是作为上面的卡片式布局中的水果点击后打开的的展示页面,可折叠式的标题栏效果需要借助CollapsingToolbarLayout。
我们将会使用CollapsingToolbarLayout实现如下图所示效果:
1.CollapsingToolbarLayout
顾名思义CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它也是Material库提供的,它可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,而是能够实现非常华丽的效果。
不过,CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为AppBarLayout的直接子布局来使用。而AppBarLayout又必须是CoordinatorLayout的子布局。
1.1.创建新Activity并编写activity_main.xml
右键包名 -> New -> Activity -> Empty Activity新建一个FruitActivity,然后编写activity_fruit.xml中的代码:
<!-- CoordinatorLayout作为最外层布局 -->
<androidx.coordinatorlayout.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=".FruitActivity">
<!-- CoordinatorLayout中嵌套AppBarLayout -->
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
<!-- 高度250dp视觉效果较好,不固定 -->
android:layout_height="250dp"
<!-- 表示控件会出现在系统状态栏中,充分利用状态栏空间 -->
android:fitsSystemWindows="true">
<!-- 在AppBarLayout中嵌套CollapsingToolbarLayout -->
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
<!-- 和之前的ToolBar相同的主题,原因相同 -->
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
<!-- 用于指定CollapsingToolbarLayout在趋于折叠状态—— -->
<!-- ——以及折叠之后的背景色,折叠后其实就是普通的Toolbar -->
app:contentScrim="@color/purple_500"
<!-- scroll表示CollapsingToolbarLayout随着水果—— -->
<!-- ——内容详情的滚动一起滚动 -->
<!-- exitUntilCollapsed表示随着滚动完成折叠之后就—— -->
<!-- ——保留在界面上,不再移出屏幕 -->
app:layout_scrollFlags="scroll|exitUntilCollapsed"
<!-- 表示控件会出现在系统状态栏中,充分利用状态栏空间 -->
android:fitsSystemWindows="true">
<!-- 定义标题栏具体内容ImageView和Toolbar -->
<!-- 意味着这个高级版的标题栏是由普通标题栏和图片组合而成 -->
<ImageView
android:id="@+id/fruitImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
<!-- 指定当前控件在CollapsingToolbarLayout—— -->
<!-- ——的折叠过程中的折叠模式 -->
<!-- parallax表示在折叠过程中产生一定错位偏移 -->
app:layout_collapseMode="parallax"
<!-- 表示控件会出现在系统状态栏中,充分利用状态栏空间 -->
android:fitsSystemWindows="true"
/>
<androidx.appcompat.widget.Toolbar
android:id="@id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
<!-- pin表示折叠过程中始终位置不变 -->
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<!-- ***标题栏布局编写完成*** -->
</com.google.android.material.appbar.AppBarLayout>
<!-- ***下面编写水果内容详情部分*** -->
<!-- NestedScrollView在ScrollView基础上—— -->
<!-- ——增加嵌套响应滚动事件功能 -->
<!-- 由于CollapsingToolbarLayout本身可以响应滚动事件 -->
<!-- 所以它的内部就需要使用NestedScrollView或RecyclerView这样的布局 -->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
<!-- 指定布局行为 -->
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<!-- 不管是NestedScrollView还是ScrollView -->
<!-- 它们的内部只允许一个直接子布局 -->
<!-- 所以通畅先嵌套一个LinearLayout -->
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 卡片式布局 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="35dp"
app:cardCornerRadius="4dp">
<!-- 显示水果详情内容 -->
<TextView
android:id="@+id/fruitContentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
/>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- 悬浮按钮 -->
<!-- 它与AppBarLayout和NestedScrollView是平级的 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_comment"
<!-- 指定锚点 appBar -->
<!-- 这样悬浮按钮就会出现在标题栏(appBar)区域内 -->
app:layout_anchor="@id/appBar"
<!-- 指定悬浮按钮在标题栏区域右下角 -->
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
1.2.修改Activity代码,编写逻辑功能
修改FruitActivity中代码:
class FruitActivity : AppCompatActivity() {
companion object {
const val FRUIT_NAME = "fruit_name"
const val FRUIT_IMAGE_ID = "fruit_image_id"
}
override fun onCreate(...) {
super.onCreate(...)
setContentView(R.layout.activity_fruit)
// 获取传入的水果名和图片id
val fruitName = intent.getStringExtra(FRUIT_NAME) ?: ""
val fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID, 0)
// 将toolbar替换ActionBar
setSupportActionBar(toolbar)
// 显示返回按钮
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// 设置页面标题
collapsingToolbar.title = fruitName
// 将图片加载到ImageView
Glide.with(this).load(fruitImageId).into(fruitImageView)
// 设置水果内容详情,水果名重复500遍
fruitContentText.text = generateFruitContent(fruitName)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
// 点击返回按钮关闭当前页面
finish()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun generateFruitContent(fruitName: String) =
fruitName.repeat(500)
}
1.3.设置RecyclerView点击事件
修改FruitAdapter代码:
class FruitAdapter(val context: Context, val fruitList: List<Fruit>) :
RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) :
ViewHolder {
val view = LayoutInflater.from(context).iflate(R.layout.fruit_item,
parent, false)
val holder = ViewHolder(view)
holder.itemView.setOnClickListener {
val position = holder.adapterPosition
val fruit = fruitList[position]
val intent = Intent(context, FruitActivity::class.java).apply {
putExtra(FruitActivity.FRUIT_NAME, fruit.name)
putExtra(FruitActivity.FRUIT_IMAGE_ID, fruit.imageId)
}
context.startActivity(intent)
}
return holder
}
...
}
现在重新运行程序,并点击卡片布局中的水果卡片,即可进入FruitActivity,此时上下滑动页面,可以看到很Material的UI体验!
1.4.充分利用状态栏空间
虽说现在水果详情展示界面效果已经非常华丽了,但是可以发现水果的背景图片和系统的状态栏总有一些不搭的感觉,如果能将背景图和状态栏融合到一起,这个视觉体验绝对提升好几个档次。
想要让背景图能够和系统状态栏融合,需要借助android:fitsSystemWindows这个属性来实现。在CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout这种嵌套的布局中,将控件的android:fitsSystemWindows设置成true,就表示该控件会出现在系统状态栏里,对应到程序中,就是水果标题栏中的ImageView应该设置这个属性了,不过必须给ImageView布局结构的所有父布局都设置上这个属性才可以,上面的activity_fruit.xml中已经包含这个属性。
1.4.1.设置主题透明色
然后还需要在程序的主题中将状态栏颜色指定成透明色才行。在主题中将android:statusBarColor属性的值指定成**@android:color/transparent**就可以了。
打开res/values/theme.xml文件,对主题内容进行修改:
<resources>
...
<style name="FruitActivityTheme" parent="Theme.MaterialTest">
<item name="android:statusBarColor">
@android:color/transparent
</item>
</style>
</resources>
1.4.2.应用主题
修改AndroidManifest.xml代码:
<manifest ...>
<application ...>
<activity
android:name=".FruitActivity"
android:theme="@style/FruitActivityTheme">
</activity>
</application>
</manifest>