前言
书接上回...
现在,我们的标题栏虽然是 Toolbar,但它看起来和传统的 ActionBar 并没有什么区别。它只能随着 RecyclerView 列表的滚动而滚动。实际上,我们可以定制标题栏的样式。
接下来,我们就借助 CollapsingToolbarLayout 来实现一个可折叠的标题栏。
CollapsingToolbarLayout
CollapsingToolbarLayout 是一个由 Material 库提供的布局,它可以丰富 Toolbar 的效果。我们来看看它的基本用法。
CollapsingToolbarLayout并不能单独存在,它必须作为AppBarLayout的直接子布局。而AppBarLayout又必须是CoordinatorLayout的子布局
我们开始实现。首先,创建一个 FruitActivity 作为水果详情界面,其布局名为 activity_fruit.xml。
实现水果标题栏
先来实现标题栏部分的布局。
首先,使用 CoordinatorLayout 作为最外层布局。它可协调其子 View 的动作。
<?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.coordinatorlayout.widget.CoordinatorLayout>
其中定义了 xmlns:app 命名空间。
然后,在 CoordinatorLayout 中放置一个 AppBarLayout,在 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.Material3.Dark.ActionBar"
app:contentScrim="?attr/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 折叠成普通标题栏时显示的背景色。
app:layout_scrollFlags 属性用于指定 AppBarLayout 响应滚动事件的逻辑。scroll 表示可滚动,没有这个标志其他的标志都不生效。exitUntilCollapsed 表示 AppBarLayout 会随着内容的滚动而向上滚动,直到完全折叠,然后固定在顶部。
现在,我们在 CollapsingToolbarLayout 中添加标题栏内容:一张图片和一个普通的 Toolbar。
<ImageView
android:id="@+id/fruitImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/fruit_image_description"
android:scaleType="centerCrop"
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" />
其中的 app:layout_collapseMode 属性是用于指定 CollapsingToolbarLayout 内部的子 View 在折叠过程中的行为。
-
pin表示在折叠过程中,Toolbar的位置始终保持不变,“钉”在内容界面的顶部。 -
parallax表示在折叠过程中,图片会比内容滚动得更慢进行移动,增加视觉层次感。
实现水果内容详情
下面实现内容详情部分的布局。
我们在 CoordinatorLayout 中,AppBarLayout 的下方(同层级),添加一个 NestedScrollView。
<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: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 的滚动,这是因为我们通过 app:layout_behavior 属性给 NestedScrollView 指定了一个 Behavior。它会在 NestedScrollView 滚动时,让 CoordinatorLayout 捕获滚动事件并通知给 AppBarLayout 做出响应。
最后,我们添加一个表示评论的悬浮按钮,它也能和 CoordinatorLayout 协作。
<?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.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="评论"
android:src="@drawable/ic_comment"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
其中使用了 app:layout_anchor 属性设置了一个锚点在 appBar 上,并使用 app:layout_anchorGravity 属性进行定位。这样 CoordinatorLayout 能够调度悬浮按钮的行为,让其根据 appBar 的位置变化自动调整自身的位置,从而始终处于标题栏区域的右下角。
实现水果详情页的功能逻辑
水果详情页的布局编写完后,我们现在来 FruitActivity 中实现逻辑。
class FruitActivity : AppCompatActivity() {
companion object {
const val FRUIT_NAME = "fruit_name"
const val FRUIT_IMAGE_ID = "fruit_image_id"
}
private lateinit var binding: ActivityFruitBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFruitBinding.inflate(layoutInflater)
setContentView(binding.root)
// 设置操作栏
setSupportActionBar(binding.toolbar)
// 显示 Home 按钮,默认为返回箭头
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// 获取传入的水果名称和水果图片资源 id
val fruitName = intent.getStringExtra(FRUIT_NAME) ?: ""
val fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID, 0)
// 填充到界面中
binding.collapsingToolbar.title = fruitName
Glide.with(this).load(fruitImageId).into(binding.fruitImageView)
binding.fruitContentText.text = generateFruitContent(fruitName)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// 点击了 Home 按钮
android.R.id.home -> {
finish()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun generateFruitContent(fruitName: String) = fruitName.repeat(500)
}
为了能从列表中跳转到 FruitActivity,我们需要给 RecyclerView 列表子项注册点击事件。
// FruitAdapter.kt
class FruitAdapter :
ListAdapter<Fruit, FruitAdapter.ViewHolder>(FruitDiffCallback) {
inner class ViewHolder(private val binding: FruitItemBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
// 在 ViewHolder 初始化时设置监听器
binding.root.setOnClickListener {
val position = bindingAdapterPosition // 获取 ViewHolder 在适配器中的位置
// 检查 position 是否有效
if (position != RecyclerView.NO_POSITION) {
// 携带数据跳转到 FruitActivity
val fruit = getItem(position)
val intent = Intent(binding.root.context, FruitActivity::class.java).apply {
putExtra(FruitActivity.FRUIT_NAME, fruit.name)
putExtra(FruitActivity.FRUIT_IMAGE_ID, fruit.imageId)
}
binding.root.context.startActivity(intent)
}
}
}
...
}
...
}
运行程序,点击任意一个水果,即可看到:
充分利用系统状态栏空间
虽然这样效果已经很不错了,但我们还可以让背景图片延伸到系统状态栏下方,实现沉浸式体验。
首先,在 FruitActivity 的 onCreate() 方法中调用 enableEdgeToEdge() 方法。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 让内容布局延伸到系统栏(状态栏、导航栏)
enableEdgeToEdge()
...
}
这样会导致一个问题:状态栏会遮挡 Toolbar 的部分区域。为此,我们需要在 FruitActivity 中添加如下内容:
class FruitActivity : AppCompatActivity() {
...
// 防止 Insets 多次应用
private var insetsApplied = false
override fun onCreate(savedInstanceState: Bundle?) {
...
ViewCompat.setOnApplyWindowInsetsListener(binding.appBar) { _, windowInsets ->
if (!insetsApplied) {
// 获取状态栏的高度
val statusBarHeight =
windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
// 增加 Toolbar 的物理高度,让它有足够空间
val toolbar = binding.toolbar
val originalToolbarHeight = toolbar.layoutParams.height
toolbar.layoutParams.height = originalToolbarHeight + statusBarHeight
// 为 Toolbar 设置顶部内边距
toolbar.setPadding(
toolbar.paddingLeft,
statusBarHeight,
toolbar.paddingRight,
toolbar.paddingBottom
)
// 为 CollapsingToolbarLayout 设置最小高度,以适应 Toolbar
binding.collapsingToolbar.minimumHeight = originalToolbarHeight + statusBarHeight
// 标记为已处理,防止重复执行
insetsApplied = true
}
// 返回原始 insets,不做消耗
windowInsets
}
}
...
}
再次运行程序,你将看到: