目录
一、什么是Material Design
Material Design
是由Google
的设计工程师基于传统优秀的设计原则,结合丰富的创意和科学技术所开发的一套全新的界面设计语言,包含了视觉、运行、互动效果等特性。 那么Google
凭什么认为Material Design
就能解决Android
平台界面风格不统一的问题呢?一言以蔽之,好看!
为了做出表率,Google
从Android 5.0
系统开始,就将所有内置的应用都应用都使用Material Design
风格进行设计。
二、Toolbar
已经将ActionBar
隐藏起来了,看下如何使用Toolbar
来替代ActionBar
,修改activity_main.xml
代码:
<?xml version="1.0" encoding="utf-8"?>
<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>
修改MainActivity.kt
代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
}
}
这代码中很关键的代码中一句,调用setSupportActionBar()
方法并将Toolbar
的实例传入,就是使用了Toolbar
,又让它的外观与功能都和ActionBar
一致了,运行结果:
怎么修改标题栏上显示的文字,这文字是在AndroidManifest.xml
中指定的:
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
...
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="Fruits">
...
</activity>
</application>
这里给activity
增加了一个android:label
属性,用于指定在Toolbar
中显示的文字,如果没有指定的话,会默认使用application
中指定的label
文字,也就是应用的名称
Toolbar
上可以再添加一些accent
按钮,提前准备了几张按钮图标,将它们放在了drawable-xxhdpi
目录下,现在右击res
目录→New
→Android Resource Directory
→Resource type
下拉菜单选择menu
后点击“OK
”,可以自动创建一个menu
文件夹,右击menu
文件夹→New
→Menu Resource File
,创建一个toolbar.xml
→点击“OK
”,并编写如下代码:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<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>
<item>
标签来定义accent
按钮,android:id
用于指定按钮的id
,android:icon
用于指定按钮的图标,android:title
用于指定按钮的文字。
修改MainActivity.kt
代码:
class MainActivity : AppCompatActivity() {
...
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.backup -> Toast.makeText(this, "You clicked Backup", Toast.LENGTH_SHORT).show()
R.id.delete -> Toast.makeText(this, "You clicked Delete", Toast.LENGTH_SHORT).show()
R.id.settings -> Toast.makeText(this, "You clicked Settings", Toast.LENGTH_SHORT).show()
}
return true
}
}
onCreateOptionsMenu()
方法中加载了toolbar.xml
这个菜单文件,然后在onOptionsItemSelected()
方法中处理各个按钮的点击事件,运行结果:
三、滑动菜单
1、DrawerLayout
<?xml version="1.0" encoding="utf-8"?>
<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
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>
<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>
这代码,最外层的控件使用了DrawerLayout
。DrawerLayout
中放置了两个直接子控件:第一个控件是FrameLayout
,用于作为主屏障中显示的内容,当然里面还有刚刚定义的Toolbar
,第二个控件是一个TextView
,用于作为滑动菜单中显示的内容,其实使用什么都可以,DrawerLayout
并没有限制只能使用固定的控件
但是关于第二个子控件有一点需要注意,layout_gravity
这个属性是必须指定的,因为我们需要告诉DrawerLayout
滑动菜单是在屏障的左边还是右边,指定left
表示滑动菜单在左边,指定right
表示滑动菜单在右边。这里我指定了start
,表示会根据系统语言进行判断
运行结果,然后在屏障的左侧边缘向右拖动,就可以让滑动菜单显示出来了
Material Design
建议的做法是在Toolbar
的最左边加入一个导航按钮,点击按钮也会将滑动菜单的内容展示出来,这样就相当于给用户提供了两种打开滑动菜单的方式,防止一些用户不知道屏障的左侧边缘是可以拖动的。
来实现这个功能,先准备了一张导航按钮的图标ic_menu.png
,将它放在了drawable-xxhdpi
目录下,修改MainActivity.kt
代码
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
//***增加代码***
supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
it.setHomeAsUpIndicator(R.drawable.ic_menu)
}
//******
}
...
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
//***增加代码***
android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
//***
...
}
return true
}
}
调用supportActionBar
方法得到了ActionBar
的,调用了setDisplayHomeAsUpEnabled()
方法让导航按钮显示出来,调用setHomeAsUpIndicator()
方法来设置一个导航按钮图标。Toolbar
最左侧的这个按钮就叫作Home
按钮,它默认的图标是一个返回的箭头,含义是返回上一个Activity
。
在onOptionsItemSelected()
方法中对Home
按钮的点击事件进行处理,Home
按钮的id
永远都是android.R.id.home
。调用drawerLayout
的openDrawer()
方法滑动菜单展示出来,注意openDrawer()
方法要求传入一个Gravity
参数,为了保证这里的行为和XML
中定义的一致,传入了GravityCompat.START
2、NavigationView
菜单页面仅仅使用了一个TextView
,非常简单,不过Google
给我们提供了一种更好的方法——使用NavigationView
。NavigationView
是Material
库中提供的一个控件,它不仅是严格按钮Material Design
的要求来设计的,而且可以将滑动菜单页面的实现变得非常简单
首先,既然这个控件是Material
库中提供的,那么就需要将这个库引入项目中才行,打开app/build.gradle
文件,在dependencies
闭包中添加如下内容:
dependencies {
...
implementation 'com.google.android.material:material:1.1.0'
implementation 'de.hdodenhof:circleimageview:3.0.1'
}
这里添加了两行依赖关系:第二行就是Material
库,第二行是一个开源项目CircleImageView,它可以用来轻松实现图片圆形化的功能
需要注意的是,当你引入了Material
库之后,还需要将res/values/styles.xml
文件中AppTheme
的parent
的主题改成Theme.MaterialComponents.Light.NoActionBar
,否则在使用接下来的一些控件时可能会遇到崩溃问题
在开始使用NavigationView
之前,还需要要准备好两个东西:menu
和headerLayout
。menu
是用来在NavigationView
中显示具体的菜单项的,headerLayout
则是用来在NavigationView
中显示头部布局的。
先来准备menu
,我事先找了几张图片作为按钮的图标,并将它们放在了drawable-xxhdpi
目录下。右击menu
文件夹→New
→Menu Resource File
,创建一个nav_menu.xml
文件,并编写如下代码:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<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="Task" />
</group>
</menu>
然后右击layout
文件夹→New
→Layout Resource File
,创建一个nav_header.xml
文件并在Root element
选择RelativeLayout
,修改其中的代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
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/ic"
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="156***@xxx.com"
android:textColor="#FFF"
android:textSize="14sp"/>
<TextView
android:id="@+id/userText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/mailText"
android:text="Tony green"
android:textColor="#FFF"
android:textSize="14sp"/>
</RelativeLayout>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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
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>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header"
app:menu="@menu/nav_menu" />
</androidx.drawerlayout.widget.DrawerLayout>
修改MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
it.setHomeAsUpIndicator(R.drawable.ic_menu)
}
//***新增代码***
navView.setCheckedItem(R.id.navCall)
navView.setNavigationItemSelectedListener {
drawerLayout.closeDrawers()
true
}
//******
}
...
}
setCheckedItem()
方法将Call菜单项设置为默认选中,setNavigationItemSelectedListener
方法来设置一个菜单项选中事件的监听器,DrawerLayout的closeDrawers()
方法将滑动菜单关闭,并返回true
表示此事件已被处理。运行结果,点击一下Toolbar
左侧的导航按钮,如下图:
四、悬浮按钮和可交互提示
1、FloatingActionButton
FloatingActionButton
是Material
库中提供的一个控件,这个控件可以帮助我们比较轻松地实现悬浮按钮的效果
仍然需要提前准备好一个图标,这里我放在了一张ic_done.png
到drawable-xxhdpi
目录下,然后修改activity_main.xml
中的代码:
<?xml version="1.0" encoding="utf-8"?>
<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
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" />
<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" />
</FrameLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
运行结果:
一个漂亮的悬浮按钮就在屏障的右下方出现了
如果你仔细观察的话,会发现这个悬浮按钮的下面还有一个阴影。其实这很好理解,因为FloatingActionButton
是悬浮在当前界面上,既然是悬浮,那么理所应当会有投影,Material
库中这种细节都帮我们考虑到了
还可以在FloatingActionButton
属性中增加悬浮高度:app:elevation="8dp"
,高度值越大,投影范围也越大。在activity
中的悬浮按钮可以点击事件
2、Snackbar
修改MainActivity.kt
中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
fab.setOnClickListener { view ->
Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT).setAction("Undo") {
Toast.makeText(this, "Data,restored", Toast.LENGTH_SHORT).show()
}.show()
}
}
...
}
Snackbar
从屏障底部出现了,上面有我设置的提示文字,还有一个“Undo
”按钮,按钮是可以点击的。过一段时间后,Snackbar
会自动从屏障底部消失
不管是出现还是消失,Snackbar
都是带有动画效果的,因此视觉体验也会比较好
不过,你有没有发现一个bug?这个Snackbar
竟然将悬浮按钮给遮挡住了,有没有什么办法能解决一下呢?当然有了,只需要借助CoordinatorLayout
就可以轻松解决
3、CoordinatorLayout
CoordinatorLayout
可以说是一个加强版的FrameLayout
,由AndroidX
库提供。它在普通情况下的作用和FrameLayout
基本一致,但是它拥有一些额外的Material
能力
事实上,CoordinatorLayout
可以监听其所有子控件的各种事件,并自动帮助我们做出最为合理的响应。举个简单的例子,刚才弹出的Snackbar
提示将悬浮按钮遮挡住了,而如果我们能让CoordinatorLayout
监听到Snackbar
的弹出事件,那么它会自动将内部的FloatingActionButton
向上偏移,从而确保不会被Snackbar
遮挡
至于CoordinatorLayout
的使用也非常简单,只需要要将原来的FrameLayout
替换一下就可以了。修改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">
<androidx.coordinatorlayout.widget.CoordinatorLayout
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" />
<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"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
由于CoordinatorLayout
本身就是一个加强版的FrameLayout
,因此这种替换不会有任何的副作用。运行结果:
可以看到,悬浮按钮自动向上偏移了Snackbar
的同等高度,从而确保不会遮挡。当Snackbar
消失的时候,悬浮按钮会自动向下偏移回到原来的位置
不过我们回过头再思考一下,刚才说的是CoordinatorLayout
可以监听其所有子控件的各种事件,但是Snackbar
好像并不是CoordinatorLayout
的子控件吧,为什么它却可以被监听到呢?
其实道理很简单,还记得我们在Snackbar
的make()
方法中传入的第一参数吗?这个参数就是用来指定Snackbar
是基于哪个View
触发的,刚才我们传入的是FloatingActionButton
本身,而FloatingActionButton
是CoordinatorLayout
中的子控件,因此这个事件就理所应当能被监听到了。你可以自己再做个实验,如果给Snackbar
的make()
方法传入一个DrawerLayout
,那么Snackbar
就会再次遮挡悬浮按钮,因为DrawerLayout
不是CoordinatorLayout
的子控件,CoordinatorLayout
也就无法监听到Snackbar
的弹出和隐藏事件了。
五、卡片式布局
1、MaterialCardView
MaterialCardView
是用于实现卡片式布局效果的重要控件,由Material
库提供。实现上,MaterialCardView
也是一个FrameLayout
,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉
右击layout
文件夹→New
→Layout Resource File
,创建一个card_item.xml
文件并在Root element
输入MaterialC
时下拉菜单选择com.google.android.material.card.MaterialCardView
,修改代码:
<?xml version="1.0" encoding="utf-8"?>
<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="10dp"
app:cardCornerRadius="10dp"
app:cardElevation="10dp"
app:contentPadding="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Card"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="世界上根本就不存在完美的事物,我们没必要浪费大量的精力去寻找不存在的东西。与其用一生的时间去执着地追求虚无缥缈的东西,不如珍惜和把握现在美好的生活。
当我们抛开追求完美的幻想和错觉,收获的可能是埋藏在平凡和朴实生活中的幸福" />
</com.google.android.material.card.MaterialCardView>
布局预览,如下图:
这个定义了一个MaterialCardView
布局,app:cardCornerRadius
属性指定卡片圆角的弧度,数值越大,圆角的弧度也越大。app:cardElevation
属性指定卡片的高度:高度值越大,投影范围也越大,但是投影效果越淡;高度值越小,投影范围也越小,但是投影效果越浓。这一点和FloatingActionButton
是一致的
接下来开始具体的代码实现,修改activity_main.xml
中的代码:
<?xml version="1.0" encoding="utf-8"?>
<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">
<androidx.coordinatorlayout.widget.CoordinatorLayout
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" />
<!-- ***新增代码*** -->
<androidx.core.widget.NestedScrollView
android:id="@+id/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="match_parent"
android:orientation="vertical"
android:paddingTop="24dp">
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- ****** -->
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
这里我们在CoordinatorLayout
中添加了一个NestedScrollView
,给它指定一个id
,然后将宽度和高度都设置为match_parent
,这样NestedScrollView
就占满了整个布局的空间,的include
就是直接导入小布局,运行结果,如下图:
可以看到,这个视图展示出来了。每两行内容都是在一张单独的卡片当中的,并且还拥有圆角和投影,是不是非常美观?
不过,还有一个bug
,Toolbar
怎么不见了,仔细观察一下原来是被NestedScrollView
给挡住了。这个问题又该怎么解决呢?这就需要借助另外一个工具了——AppBarLayout
2、AppBarLayout
修改activity_main.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<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">
<androidx.coordinatorlayout.widget.CoordinatorLayout
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">
<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" />
</com.google.android.material.appbar.AppBarLayout>
<!-- ****** -->
<androidx.core.widget.NestedScrollView
android:id="@+id/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="match_parent"
android:orientation="vertical"
android:paddingTop="24dp">
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
在NestedScrollView
中使用app:layout_behavior
属性指定了一个布局行为,appbar_scrolling_view_behavior
这个字符串也是由Material
库提供的
现在重新运行一下程序,你就会发现一切都正常了,如下图:
虽说使用AppBarLayout
已经成功解决了NestedScrollView
遮挡Toolbar
的问题,但是刚才提到过,AppBarLayout
中应用了一些Material Design
的设计理念,好像从上面的例子完全体现不出来呀。事实上,当NestedScrollView
滚动的时候就已经将滚动事件通知给AppBarLayout
了,只是我们还没进行处理而已。那么下面就让我们来进一步优化,看看AppBarLayout
到底能实现什么样的Material Design
效果
当AppBarLayout
接收滚动事件的时候,它内部的子控件其实是可以指定如何去响应这些事件的,通过app:layout_scrollFlags
属性就能实现,修改activity_main.xml
中的代码:
这里在Toolbar
中添加了一个app:layout_scrollFlags
属性,并将这个属性的值指定成了scroll|enterAlways|snap
,其中,scroll
表示当NestedScrollView
向上滚动的时候,Toolbar
会跟着一起向上滚动并实现隐藏,enterAlways
表示当NestedScrollView
向下滚动的时候,Toolbar
会跟着一起向下滚动并重新显示,snap
表示当Toolbar
还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏还是显示
改了只有这一行代码而已,现在重新运行一下程序,并向上下滚动NestedScrollView
,如下图:
可以看到,随着我们向上滚动NestedScrollView
,Toolbar
竟然消失了!而向下滚动NestedScrollView
,Toolbar
又会重新出现,这其实也是Material Design
中的一项重要设计思想,因为当用户在向上滚动NestedScrollView
的时候,其注意力肯定是在NestedScrollView
的内容上的,这个时候如果Toolbar
还占据着屏障空间,就会在一定程度上影响用户的阅读体验,而将Toolbar
隐藏则可以让阅读体验达到最佳状态。当用户需要操作Toolbar
上的功能时,只需要要轻微向下滚动,Toolbar
就会重新出现。这种设计方式既保证了用户的最佳阅读效果,又不影响任何功能上的操作,Material Design
考虑得就是这么细致入微
当然了,像这种功能,如果是使用ActionBar
,那就完全不可能实现了,Toolbar
的出现为我们提供了更多的功能
六、可折叠式标题栏
虽说我们现在的标题栏是使用Toolbar
来编写的,不过它看上去和传统
1、CollapsingToolbarLayout
右击com.example.myapplication3
包→New
→Kotlin Class/File
→输入CollapsingActivity
(默认Class
)后回车,就创建了一个CollapsingActivity
,并将布局名指定成activity_collapsing.xml
,然后开始编写可折叠式标题栏详情展示界面的布局
由于整个布局文件比较复杂,这里我准备采用分段编写的方式。activity_collapsing.xml
中的内容主要分为两部分,一个是风景标题栏,一个是内容详情,我们来一步步实现
首先实现标题栏部分,这里使用ConstraintLayout
作为最外层布局,如下代码:
<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">
</androidx.coordinatorlayout.widget.CoordinatorLayout>
一开始的代码还是比较简单的,相信没有什么需要解释的地方。注意的始终记得定义一个xmlns:app
的命名空间,在Material Design
的开发中会经常用到它
接着在CoordinatorLayout
中嵌套一个AppBarLayout
,如下代码:
<?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:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
目前为止也没有什么难理解的地方,我们给AppBarLayout
定义了一个id
接下来在AppBarLayout
中再嵌套一个CollapsingToolbarLayout
,如下代码:
<?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:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
<!-- ***新增代码*** -->
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
</com.google.android.material.appbar.CollapsingToolbarLayout>
<!-- ****** -->
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
app:contentScrim
属性用于指定CollapsingToolbarLayout
在趋于折叠状态以及折叠之后的背景色,其实CollapsingToolbarLayout
在折叠之后就是一个普通的Toolbar
,对么背景色肯定应该是colorPrimary
了,具体的效果我们待一会儿就能看到
接下来,在CollapsingToolbarLayout
中定义标题栏的具体内容,如下代码:
<?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:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<!-- ***新增代码*** -->
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/image"
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"/>
<!-- ****** -->
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
可以看到,在CollapsingToolbarLayout
中定义了一个ImageView
和一个Toolbar
,也就意味着,这个高级版的标题栏将是由普通的标题栏加上图片组合而成的。这里定义的大多数属性我们是已经见过的,就不再解释了,只有一个app:layout_collapseMode
比较陌生。它用于指定当前控件在CollapsingToolbarLayout
折叠过程中的折叠模式,其中Toolbar
指定成pin
,表示在折叠的过程中位置始终保持不变,ImageView
指定成parallax
,表示会在折叠的过程中产生一定的错位偏移,这种模式的视觉效果会非常好
这样就将标题栏的界面编写完成了,下面开始编写内容详情部分,继续修改activity_collapsing.xml
中的代码,如下代码:
<?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:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
...
</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">
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
内容详情的最外层布局使用了一个NestedScrollView
,注意它和AppBarLayout
是平级的。,因此我们在它的内部就需要使用NestedScrollView
或RecyclerView
这样的布局。另外,这里通过app:layout_behavior
属性指定了一个布局行为,这和之前在RecyclerView
中的的用法是一模一样的
不管是ScrollView
还是NestedScrollView
,它们的内部都只允许存在一个直接子布局。因此,如果我们想要在里面放入很多东西的话,通常会先嵌套一个LinearLayout
,然后再在LinearLayout
中放入具体的内容就可以了,如下代码:
<?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">
...
<androidx.core.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="wrap_content"
android:orientation="vertical">
</LinearLayout>
<!-- ****** -->
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
android:orientation="vertical"
是垂直方向的
接下来在LinearLayout
中放入具体的内容,先准备使用一个TextView
来显示内容详情,并将TextView
放在一个卡片式布局当中,如下代码:
<?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">
...
<androidx.core.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="wrap_content"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="35dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
app:cardCornerRadius="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="世界上根本就不存在完美的事物,我们没必要浪费大量的精力去寻找不存在的东西。与其用一生的时间去执着地追求虚无缥缈的东西,不如珍惜和把握现在美好的生活。
当我们抛开追求完美的幻想和错觉,收获的可能是埋藏在平凡和朴实生活中的幸福"/>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
编写完了,不过我们还可以在界面上再添加一个悬浮按钮。这个悬浮按钮并不是必需的,根据具体的需求添加就可以了,如果加入的话,我们将获得一些额外的动画效果
为了做出示范,我就准备在activity_collapsing.xml
中加入一个悬浮按钮了。这个界面是一个详情展示界面,那么我就加入一个表示评论作用的悬浮按钮吧。首先需要提前准备好一个图标,这里我放置了一张ic_comment.xml
到drawable-xxhdpi
目录下,然后修改activity_collapsing.xml
中的代码:
<?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:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
...
</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">
...
</androidx.core.widget.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"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
可以看到,这里加入了一个FloatingActionButton
,它和AppBarLayout
以及NestedScrollView
是平级的。FloatingActionButton
中使用app:layout_anchor
属性指定了一个锚点,我们就将锚点设置为AppBarLayout
,这样悬浮按钮就会出现在标题栏的区域内,接着又使用app:layout_anchorGravity
属性将悬浮按钮定位在标题栏区域的右下角。其他一些属性比较简单,就不再进行解释了
好了,现在我们终于将整个activity_collapsing.xml
布局都编写完了,内容虽然比较长,但由于是分段编写的,并且每一步我都进行了详细的说明,相信你应该看得很明白吧
界面完成了之后,接下来我们开始编写功能逻辑,修改ImageActivity.kt中的代码:
class ImageActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_collapsing)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
finish()
return true
}
}
return super.onOptionsItemSelected(item)
}
}
使用了Toolbar
的标准用法,将它作为ActionBar显示,并启用Home按钮。由于Home按钮的默认图标就是一个返回箭头,这正是我们所期望的,因此就不用额外设置的图标了
接下来开始填充界面上的内容,调用CollapsingToolbarLayout
的setTitle()方法,将应用名设置当前界面的标题,ImageView
的src
获取这image设置到标题栏的ImageView
上面。
最后,我们在onOptionsItemSelected()
方法中处理了Home
按钮的点击事件,当点击这个按钮时,就调用finish()
方法关闭当前的Activity
,从而返回上一个Activity
所有工作都完成了吗?其实还最差最关键的一步,就是处理NestedScrollView的点击事件,不然,我们无法打开ImageActivity,修改activity_main.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<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">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<androidx.core.widget.NestedScrollView
android:id="@+id/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="match_parent"
android:orientation="vertical"
android:paddingTop="24dp">
<include layout="@layout/card_item"
android:id="@+id/card"/><!-- ***新增代码*** -->
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
<include layout="@layout/card_item" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- ****** -->
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
android:id="@+id/card"
点击事件
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
//***新增代码***
card.setOnClickListener {
val intent = Intent(this@MainActivity,ImageActivity::class.java)
startActivity(intent)
}
//******
}
...
}
调用startActivity()
方法启动ImageActivity
,如下图:
2、充分利用系统状态栏空间
先看下这gif
图,你会发现背景图片和系统的状态栏总有一些不搭的感觉,如果我们能将背景图和状态栏融合到一起,那这个视觉体验绝对能提升好几个档次
不过,在Android 5.0
系统之前,我们是无法对状态栏的背景或颜色进行操作的,那个时候也还没有Material Design
的概念,但是Android 5.0
及之后的系统都是支持这个功能。恰好所有代码最低兼容的就是Android 5.0
系统,因此这里完全可以进一步地提升视觉体验
想要让背景图能够和系统状态栏融合,需要借助android:fitsSystemWindows
这个属性来实现。在CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout
这种嵌套结构的布局中,将控件的android:fitsSystemWindows
属性指定成true
,就表示该控件会出现在系统状态栏里。对应到我们的程序,那就是标题栏中的ImageView
应该设置这个属性了。不过只给ImageView
设置这个属性是没有用的,我们必须将ImageView
布局结构中的所有父布局都布局上这个属性才可以,修改activity_collapsing.xml
中的代码:
<?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"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:fitsSystemWindows="true">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/image"
app:layout_collapseMode="parallax"
android:fitsSystemWindows="true"/>
...
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
但是,即使我们将android:fitsSystemWindows
属性都设置好了也没有用,因为还必须在程序的主题中将状态栏颜色指定成透明色才行。指定成透明色的方法很简单,在主题中将android:statusBarColor
属性的值指定成@android:color/transparent
就可以了
打开res/values/styles.xml文件,对主题的内容进行修改,如下所示:
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="colorOnSecondary">@color/white</item>
</style>
<!-- ***新增代码*** -->
<style name="ImageActivityTheme" parent="AppTheme">
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
<!-- ****** -->
...
</resources>
这里我们定义了一个ImageActivityTheme
主题,它是专门给ImageActivity
使用的。ImageActivityTheme
的父主题是AppTheme
,也就是说,它继承了AppTheme
中的所有特性。在此基础之上,我们将ImageActivityTheme
中的状态栏的颜色指定成透明色
最后,还需要让ImageActivity
使用这个主题才可以,修改AndroidManifest.xml
中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication3">
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
...
<activity
android:name=".ImageActivity"
android:theme="@style/ImageActivityTheme" /><!-- ***新增代码*** -->
</application>
</manifest>
这里使用android:theme
属性单独给ImageActivity
指定了ImageActivityTheme
这个主题,这样我们就大功告成了。现在重新运行程序,如下图: