前言
书接上回...
立面设计(Elevation)是 Material Design 中一条很重要的设计思想,应用不应该只是一个平面,而是应该通过不同层级的高度来构建视觉和交互的深度,创建出立体的效果。最直观的体现就是悬浮按钮(FloatingActionButton)。
现在,我们就来学习悬浮按钮的用法,并学习一种可交互的提示工具 Snackbar,之前的 Toast 只能告知用户发生了什么,而 Snackbar 还能提供操作选项。
FloatingActionButton
FloatingActionButton 是 Material 库中的控件,它可帮助我们轻松实现悬浮按钮效果。
它默认会使用主题中的 colorPrimaryContainer 作为按钮的背景颜色,使用 colorOnPrimaryContainer 作为按钮上图标的颜色。
我们可以在 res/values/themes.xml 文件中,重写以上两个属性:
<resources>
<style name="Base.Theme.MaterialTest" parent="Theme.Material3.Light.NoActionBar">
...
<item name="colorPrimaryContainer">@color/primary_container</item>
<item name="colorOnPrimaryContainer">@color/on_primary_container</item>
</style>
<style name="Theme.MaterialTest" parent="Base.Theme.MaterialTest" />
</resources>
然后,在 res/values/colors.xml 文件中,添加这些颜色的定义:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<color name="primary_container">#f95a82</color>
<color name="on_primary_container">#fff</color>
</resources>
最后,在主屏幕内容布局中放置一个 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">
<!--主屏幕内容-->
<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="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.MaterialComponents.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:contentDescription="完成操作"
android:src="@drawable/ic_done" />
</FrameLayout>
<!--滑动菜单-->
<com.google.android.material.navigation.NavigationView
... />
</androidx.drawerlayout.widget.DrawerLayout>
现在运行程序,界面如图所示:
可以看到,右下角出现了一个悬浮按钮,它的四周还带着阴影。
我们可以通过 app:elevation 属性来指定按钮的悬浮高度。高度值越大,阴影范围也越大,但颜色越淡。反之,阴影范围越小,颜色越浓。
例如:
<com.google.android.material.floatingactionbutton.FloatingActionButton
...
app:elevation="8dp" />
我们可以给悬浮按钮添加交互功能,其实它和普通按钮完全一样,都是通过调用 setOnClickListener() 方法来注册点击事件。例如,在 MainActivity 中:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
...
binding.fab.setOnClickListener {
Toast.makeText(this, "FAB clicked", Toast.LENGTH_SHORT).show()
}
}
...
}
运行程序并点击悬浮按钮,可以在界面中看到:
Snackbar
目前,在悬浮按钮的点击事件中,我们依然是使用 Toast 作为提示工具。现在,我们就来使用更加友好的 Snackbar。
但请你注意,Snackbar 并不是 Toast 的替代品,它们的应用场景不同:Toast 用于向用户提示一段非必要、不可交互的简单信息,用户只能被动接收。而 Snackbar 对其做了扩展,可在提示中添加一个操作按钮,让用户有机会立即响应。例如,当邮件被删除后,我们可以使用它提供撤销选项。
在悬浮按钮的点击事件中弹出 Snackbar:
binding.fab.setOnClickListener { view ->
// 调用 make() 方法创建 Snackbar 对象
Snackbar.make(view, "Data has deleted", Snackbar.LENGTH_SHORT)
// 设置一个可交互的 Action
.setAction("Undo") {
Toast.makeText(this, "Data has restored", Toast.LENGTH_SHORT).show()
}
// 显示 Snackbar
.show()
}
make() 方法的第一个参数接收一个 View 对象。Snackbar 会根据这个传入的 View,向上遍历视图树,查找一个合适的父布局,用于展示提示信息。
运行程序并点击悬浮按钮,效果如下图:
可以看到,Snackbar 出现在了屏幕底部。但 Snackbar 从底部弹出时,遮挡住了我们的悬浮按钮,降低了用户体验。
要解决这个问题,需要用到 CoordinatorLayout。
CoordinatorLayout
CoordinatorLayout 可以理解为是一个“加强版”的 FrameLayout 布局,作用和 FrameLayout 类似。但它的强大在于引入了 Behavior(行为)机制,允许其子 View 之间进行复杂的交互和依赖。
它能“协调”其内部子控件的行为。例如,当它感知到 Snackbar 弹出时,能够通知 FloatingActionButton 做出相应的移动。
我们将 activity_main.xml 中的 FrameLayout 替换为 CoordinatorLayout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout ...>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
再次运行程序并点击悬浮按钮:
可以发现问题解决了:当弹出 Snackbar 时,悬浮按钮会自动向上偏移;而当 Snackbar 消失时,悬浮按钮会自动向下偏移到原来的位置。
那 CoordinatorLayout 是如何协调 Snackbar 和 FloatingActionButton 的?
FloatingActionButton 控件内部默认设置了一个 FloatingActionButton.Behavior,这个行为会关注 Snackbar 实际视图的出现和消失。
当我们调用 show() 方法弹出 Snackbar 提示时,Snackbar 会被加入到 CoordinatorLayout 中,而 CoordinatorLayout 发现有子 View 状态发生变化,会通知 FloatingActionButton。
FloatingActionButton 控件接收到通知后,其行为内部的逻辑会被触发,自动计算出 Snackbar 的高度,然后以动画形式将悬浮按向上偏移。
未完待续...