做 Android 开发的朋友,几乎都会碰到这样一个需求:做一个底部有悬浮按钮、顶部工具栏能跟随滚动优雅折叠或隐藏的页面。这两件事,正好对应了 Material Design 里两个高频组件——AppBarLayout 和 FloatingActionButton。很多人单独用这两个组件都挺熟,但一旦要把它们组合起来,就会出现各种“联动失效”“按钮不跟随”“动画生硬”的问题。
这篇文章我就把这两个组件的基本使用、组合联动、踩坑排查一次讲透,其中所有代码都是可以直接落地使用的实战写法,不是概念堆砌。项目的核心场景是一个“城市攻略详情页”:顶部是可折叠的图片头图,下方是可滚动的内容区域,右下角是一个收藏悬浮按钮。这个场景基本覆盖了这两个组件的绝大多数玩法,搞懂它,你就能应付大部分实际开发场景了。
1. 为什么这两个组件总是一起出现
1.1 组件的定位与职责划分
先说 AppBarLayout。它是 Android 官方对 Material Design 中 App Bar 概念的实现类,本质是一个垂直方向的 LinearLayout,但它的价值不在于“摆放几个按钮”,而在于它允许子视图响应滚动事件,从而改变自身的位置和尺寸。比如工具栏的透明变化、图片头图的缩放与折叠、标题滑动时的显隐切换,都是通过 AppBarLayout 这个容器统一协调的。
FloatingActionButton 则是悬浮操作按钮,继承自 ImageView,原生自带阴影和波纹效果,是页面核心操作(通常是最重要的单一操作,比如分享、收藏、添加)的视觉入口。
单独看,两者各有分工,但只要放在同一个页面里,它们自然会产生交互关系:滚动时悬浮按钮要不要跟着变小、要不要隐藏、要不要在工具栏折叠后换一个位置显示。想要实现这些效果,两个组件必须同时位于一个统一的容器内——这个容器就是 CoordinatorLayout。它可以理解为“手势总协调中心”,负责感知整个页面的滚动状态,并把这些状态分发给注册协同的控件。这也是生态里最常用的模式:CoordinatorLayout 做根布局,AppBarLayout 放头部,可滚动的正文区与 FloatingActionButton 作为兄弟节点直接挂载在根布局下,三者通过 Behavior 互相沟通。
1.2 这套组合解决了什么问题
把 AppBarLayout 与 FloatingActionButton 组合使用,核心解决了三个体验问题。
第一,是空间利用问题。手机屏幕是固定的,内容越丰富,留给工具栏和按钮的空间越有限。通过折叠头部、滚动时隐藏悬浮按钮,可以为正文内容腾出有效展示面积。比如用户下滑阅读时,头图会逐渐收窄变成一条紧凑的工具栏,而不再占满大半个屏幕。
第二,是操作可达性问题。悬浮按钮如果固定在页面左上角,当你滚到页面底部时,拇指需要跨越整个屏幕去点击,很不顺手。通过 CoordinatorLayout 的协调布局,悬浮按钮可以跟随当前阅读区域做位置微调,甚至在你冲向底部内容时自动让位,保证核心操作始终在合理位置。
第三,是视觉层级问题。Material Design 强调层级关系,AppBarLayout 的折叠动画和 FAB 的显隐变化,都是页面响应滚动事件的即时反馈。用户滚动时,界面元素不再是“静止的纸片”,而是有节奏地改变形态,这种动态反馈带来的使用体验会显著优于静态页面。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心联动机制与布局设计思路
2.1 CoordinatorLayout 与 Behavior 的工作方式
要理解联动,绕不开 Behavior,这个概念的翻译是“行为”。它本质上是一个观察者——监听滚动事件或布局位置变化,然后对这个观察结果做出响应。CoordinatorLayout 在测量和布局时,会遍历所有子视图,检查它们是否实现了针对某个目标视图的 Behavior,如果实现了,就会在目标视图的位置、尺寸、滚动偏移量发生变化时,回调对应的处理逻辑。
AppBarLayout 的滚动折叠,依赖的是其内部子视图上设置的 LayoutParams 滚动标志(scrollFlags)。而 FloatingActionButton 的自动显隐,依赖的是它自带的一个内部 Behavior,这个 Behavior 会监听 AppBarLayout 的折叠状态或正文区域的滚动位移,当位移方向向下(下滑浏览)时隐藏按钮,当位移方向向上(上滑回看)时再显示按钮。
这里有个很重要的点:CoordinatorLayout 不是自动就能感知滚动的。它必须接收到“嵌套滚动”事件,而默认的 ScrollView 并不会主动发送这类事件。这也是我在文后会反复强调 NestedScrollView 的原因——只有它和 RecyclerView 这类基于嵌套滚动机制的控件,才能把自己的滚动位移上报给 CoordinatorLayout。
2.2 关键参数:scrollFlags 逐项拆解
AppBarLayout 的子视图是否响应滚动,具体响应到哪种程度,全部由 scrollFlags 这一个参数控制。常见的取值组合有五种,我放在一个表里对比:
| scrollFlags 取值 | 滚动表现 | 适用场景 |
|---|---|---|
| scroll | 子视图随内容滚出屏幕 | 最基础的联动模式 |
| scroll | enterAlways | 下拉时立即出现,上滑时立即消失 | 搜索栏、快速工具栏 |
| scroll | exitUntilCollapsed | 滚出时到达某个最小高度后停住 | 折叠工具栏模式 |
| scroll | enterAlwaysCollapsed | 配合 exitUntilCollapsed,下拉时先展开到最小高度再展开到全高 | 少用,多用于复杂交互 |
| 不设置 scroll | 子视图永远固定在原位置,不参与滚动 | 顶部常驻指示器 |
实际项目里最常用的两个组合是:作为普通工具栏时用 scroll|enterAlways,配合 CollapsingToolbarLayout 时用 scroll|exitUntilCollapsed|snap。snap 是一个吸附效果——滚动停止后,视图要么自动滚回完全展开,要么滚到完全折叠,不会尴尬地停在半路。
2.3 布局结构参考
我这次“城市攻略详情页”目标效果是:头图随内容上滑逐渐折叠成一条紧凑标题栏,正文是一个 NestedScrollView,右下角悬浮一个收藏按钮,往下滑内容时按钮隐藏,往上滑回看时按钮重新出现。
基于这个需求,根布局应选择 android.support.design.widget.CoordinatorLayout(如果是 AndroidX 项目则对应 com.google.android.material.appbar.AppBarLayout,下文代码全部以 AndroidX 写法为例)。整体结构如下:
xml复制<androidx.coordinatorlayout.widget.CoordinatorLayout ...>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
...>
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:id="@+id/headerImage"
android:scaleType="centerCrop"
... />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
...>
<!-- 正文内容 -->
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabFavorite"
android:src="@drawable/ic_favorite"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|end"
... />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
注意几个细节:正文区 NestedScrollView 必须设置 layout_behavior="@string/appbar_scrolling_view_behavior",这个字符串定义在 Material 库中,作用是告诉 CoordinatorLayout,这块内容区域需要根据 AppBarLayout 的折叠状态调整自身的高度和位置,同时也是滚动事件向上传递的关键通道。如果你漏了这一句,页面会变成“只有 AppBar 在动,正文不动”的诡异状态。
FloatingActionButton 我这边使用了 layout_anchor 方式把它锚定到 AppBarLayout 上,再通过 layout_anchorGravity 让它落在头图区域的右下角。这样当 AppBarLayout 的显示位置随滚动移动时,按钮也会跟着移动,视觉上会给人一种“按钮从头图区域里慢慢退场”的感觉,这是 Material Design 里一种很常见的悬浮按钮与头部联动的做法。
3. 实操过程与核心实现细节
3.1 搭建基础布局
按照上一节的骨架,下面给出一个可以直接运行的最小布局文件。这里我刻意把图片头图高度设成 260dp,折叠后的工具栏高度保留为 56dp(MaterialToolbar 的默认高度),这样折叠动画的视觉跨度足够明显。
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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="260dp"
app:contentScrim="@color/colorPrimary"
app:titleEnabled="true"
app:expandedTitleMarginStart="48dp"
app:expandedTitleMarginBottom="32dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:id="@+id/headerImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/header_desc"
android:scaleType="centerCrop"
android:src="@drawable/header_placeholder"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.7" />
<com.google.android.material.appbar.MaterialToolbar
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.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp"
android:text="@string/article_content"
android:textSize="16sp"
android:lineSpacingExtra="6dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabFavorite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/fab_favorite_desc"
android:src="@drawable/ic_favorite_border"
app:backgroundTint="@color/colorAccent"
app:elevation="8dp"
app:fabSize="normal"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|end"
app:rippleColor="@color/ripple_white"
app:tint="@android:color/white"
app:useCompatPadding="true" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
这里我用的图片头图 layout_collapseMode="parallax",它可以产生视差效果:图片本身会以 0.7 倍速跟手滚动,也就是“头图比折叠进度慢半拍”的视觉效果,立体感更强。MaterialToolbar 用 layout_collapseMode="pin" 固定住了,折叠时不会滚出屏幕,而是像被钉在顶部一样停在那里。
FloatingActionButton 的 useCompatPadding 设置为 true,在部分国产设备上如果不加会吃掉阴影边距,导致视觉上按钮边缘被裁剪。加上这个参数既是保险,也能让悬浮按钮的外阴影完整绘制。
3.2 让 FAB 跟随滚动自动显隐
基础布局只是把 FAB 锚定到了 AppBarLayout 上,但它并不会自动“隐藏”。这种“往下滚,按钮淡出;往上滚,按钮回来”的效果,靠的是安卓 Material 库中 FloatingActionButton 内置的 Behavior 实现。
默认情况下,在 CoordinatorLayout 中,FloatingActionButton 会监听同级的 AppBarLayout 和其他可滚动视图的嵌套滚动行为。如果是“可见性联动”,也就是按钮在页面显示区的底部出现时,你不需要写任何代码,它就能跟随 AppBarLayout 的折叠自动隐藏和出现。
但如果你想自定义它的显示位置,比如希望它不是默认的 显示在正文右下角,而是锚定在某个具体控件附近,那你需要在 Java/Kotlin 代码里设置 layout_anchor。我上面的布局中已经把锚点指到了 AppBarLayout,因此效果是:从头图展开状态到折叠状态,按钮会跟随头部位移并在折叠过程中逐渐隐藏。
如果你是在代码里动态创建 FAB,而不是写在 XML 里,可以这样设置:
kotlin复制val behavior = FloatingActionButton.Behavior()
val params = fab.layoutParams as CoordinatorLayout.LayoutParams
params.behavior = behavior
params.anchorId = R.id.appbar
params.anchorGravity = Gravity.BOTTOM or Gravity.END
这种动态设置方式通常用于业务上需要根据权限或状态变更,动态展示或修改悬浮按钮的场景。静态布局完全不需要写这些代码,直接在 XML 里配置即可。
还有一点需要说明:默认 Behavior 对 FAB 的显隐控制,只跟随 AppBarLayout 的舒展与折叠。如果正文是可滚动的 RecyclerView,且你希望按钮在列表快速下滑时也自动隐藏、上滑时自动显示,则不能用默认 Behavior。这时需要自定义一个小逻辑,稍后文末我会给一个精简的自定义 Behavior 样例。
3.3 代码层面初始化与监听折叠状态
布局搭好只是第一步,工具栏上肯定要处理返回导航、标题变化、FAB 点击响应,以及折叠状态切换时的 UI 调整。以下是我在这个页面里实际使用过的初始化代码,已整理成可以直接复用的模板。
kotlin复制class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toolbar = findViewById<MaterialToolbar>(R.id.toolbar)
val collapsingToolbar = findViewById<CollapsingToolbarLayout>(R.id.collapsingToolbar)
val appBarLayout = findViewById<AppBarLayout>(R.id.appbar)
val fabFavorite = findViewById<FloatingActionButton>(R.id.fabFavorite)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// 设置折叠工具栏的标题显示逻辑
collapsingToolbar.title = "杭州城市攻略"
// 监听折叠状态,用于切换图标或调整透明度
appBarLayout.addOnOffsetChangedListener { appBar, verticalOffset ->
val totalScrollRange = appBar.totalScrollRange
val collapseRatio = if (totalScrollRange == 0) 0f
else -verticalOffset.toFloat() / totalScrollRange
if (collapseRatio >= 1f) {
// 已完全折叠,可切换到收缩态标题样式
supportActionBar?.title = "城市攻略"
collapsingToolbar.titleEnabled = false
} else {
supportActionBar?.title = ""
collapsingToolbar.titleEnabled = true
}
}
fabFavorite.setOnClickListener {
// 收藏逻辑
toggleFavorite()
}
}
private fun toggleFavorite() {
// 模拟收藏切换状态
}
}
监听折叠比例 collapseRatio 是我个人常用的一个手法。很多 UI 需求会要求在折叠前后切换不同文案、切换工具栏滚动背景的透明度,这个比例直接用于透明度计算非常方便。例如把标题栏背景的 alpha 百分比绑定到 collapseRatio,就可以得到一条非常顺滑的滚动渐变效果,比单纯监听折叠完成与否细腻很多。
3.4 让 FAB 真正“隐藏”的技巧
默认情况下 FAB 的自带 Behavior,会通过 AnimationUtils 做一个缩放和透明度动画来完成显示与隐藏。基础使用中,你只需要确保 FAB 是 CoordinatorLayout 的直接子节点,且不设置 layout_gravity 之类干扰布局的额外参数,它就能正常工作。
不过我实测下来,默认 Behavior 的触发逻辑对 NestedScrollView 的支持存在一个坑:如果你把 FAB 锚定到了 AppBarLayout,并且 AppBarLayout 的 scrollFlags 不包含 scroll 时,FAB 永远不会触发隐藏。因此,如果你想保留 FAB 自动隐藏效果,需要保证 AppBarLayout 的子视图处于“可滚动”状态(也就是设置了 scroll 标志)。
另外要提醒的是:千万别直接用 view.visibility = View.GONE 去隐藏 FAB。因为 FAB 的默认 Behavior 内部保存了自己的状态引用,如果你用 visibility 直接操作,Behavior 下次显示时会把已经 GONE 的按钮重新执行显隐动画,导致闪烁或者位置跳变。想要主动控制显隐,应该使用:
kotlin复制fabFavorite.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton) {
super.onHidden(fab)
// 隐藏完成后的回调
}
})
fabFavorite.show()
我见过不少人在代码里直接调用 fab.visibility = View.GONE,结果滚动时按钮一下出现一下消失,非常鬼畜。这个 hide()/show() 接口才是 FAB 官方提供的标准动画接口,它内部会正确处理时长、插值器和状态同步。
4. 常见问题与排查技巧实录
4.1 滚动事件无法传递,AppBarLayout 不响应
这是所有刚接触这套组件的人遇到的第一个问题:明明照官网代码写了,但页面就是静止的。绝大多数原因是布局里用了普通的 ScrollView 而不是 NestedScrollView。普通 ScrollView 不能向父 CoordinatorLayout 发出嵌套滚动事件,AppBarLayout 的滚动标志再齐全也无事可做。解决方案是换成 androidx.core.widget.NestedScrollView,如果用的是 RecyclerView 则天然支持,不需要改。
这里额外补充一段经验:即使你在 CoordinatorLayout 内放置了一个 NestedScrollView,如果它的父容器不是 CoordinatorLayout,或者它自己没有设置 layout_behavior,一样会失灵。最简单的自检方法:检查 NestedScrollView 上是否有 @string/appbar_scrolling_view_behavior 这条属性。没有就补上。
4.2 折叠时标题和返回按钮重叠
CollapsingToolbarLayout 默认会启用展开态标题,如果你要在折叠态显示 MaterialToolbar 自己的标题,需要处理两者关系。我在 3.3 节里用了 setTitle() 和 setTitleEnabled() 切换,这是比较标准的做法。
还有另一种更推荐的做法:直接用 MaterialToolbar 的 setTitle 作为唯一标题载体,完全禁用 CollapsingToolbarLayout 的 title 功能,再通过自定义折叠动画实现标题颜色和位置的渐变。这样能最大化避免两套标题系统互相干扰。如果项目对交互细节要求不高,建议就用 CollapsingToolbarLayout 自带标题,省心。
注意:CollapsingToolbarLayout 的 expandedTitleMarginStart 只对展开态标题有效,若改成左对齐,需要同时设置 expandedTitleGravity 和 collapsedTitleGravity 为 Gravity.START。
4.3 FAB 不跟随 AppBarLayout,总是钉在页面右下角
在 2.3 节的布局中,FAB 通过 layout_anchor="@id/appbar" 和 AppBarLayout 建立了关联。如果你希望它从头图区域跟随折叠,那么必须把锚点设置为 AppBarLayout 或其内部子控件。还有一种常见需求是让按钮固定在右下角不跟随任何控件,这时就不要设置 layout_anchor 和 layout_anchorGravity,只需要设置普通 margin 即可。
确认锚点的方法很简单:在布局预览里切换 AppBarLayout 的 toolbar 折叠状态,观察 FAB 的位置是否随折叠而变化。如果没变化,优先检查你是不是把 FAB 放进了别的容器,比如错误地放进 NestedScrollView 或 ConstraintLayout 里了。
4.4 FAB 自己不动,但 AppBar 正常折叠
这种情况说明 CoordinatorLayout 的滚动传递正常,但 FAB 的 Behavior 没有生效。原因通常是你在 XML 中给 FAB 显式设置了 layout_behavior 覆盖了默认行为,或者 FAB 被包在了 FrameLayout/LinearLayout 里面脱离了对 CoordinatorLayout 的直接依赖。
这里我给一个常见排查顺序表,照着过一遍就能定位九成问题:
| 现象 | 排查点 |
|---|---|
| AppBar 不折叠 | 滚动容器是否 NestedScrollView / RecyclerView |
| AppBar 折叠但 FAB 不消失 | FAB 是否直接挂在 CoordinatorLayout 下 |
| FAB 闪烁或跳位置 | 是否绕过 hide()/show() 直接操作 visibility |
| FAB 位置奇怪 | layout_anchorGravity 是否设成了 start |
| 折叠后标题不显示 | 是否设置了 contentScrim / title 是否开启 |
| 视差效果没有 | ImageView 的 layout_collapseMode 是否设置成 parallax |
4.5 隐藏 FAB 的进阶需求
部分产品要求:用户滚动到文章尾部时才隐藏悬浮按钮,回滚超过一定距离再显示。默认 Behavior 做不到这种阈值控制,需要自定义一个 Behavior 子类。
我封装过一个最小可用版本,直接继承自 FloatingActionButton.Behavior,复写了 onNestedScroll 方法,在滚动距离超过 300dp 时隐藏按钮,向上回滚超过 0dp 时显示:
kotlin复制class ThresholdHideBehavior(
context: Context, attrs: AttributeSet?
) : FloatingActionButton.Behavior(context, attrs) {
private var totalScrolled = 0
private var hidden = false
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
) {
super.onNestedScroll(
coordinatorLayout, child, target,
dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type
)
if (dyConsumed > 0) {
totalScrolled += dyConsumed
if (totalScrolled > 300 && !hidden) {
child.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton) {
hidden = true
}
})
}
} else if (dyConsumed < 0) {
totalScrolled = if (totalScrolled > 0) totalScrolled + dyConsumed else 0
if (totalScrolled <= 0 && hidden) {
child.show()
hidden = false
}
}
}
}
使用方式是在 XML 的 FAB 节点上添加 app:layout_behavior=".ThresholdHideBehavior"。这里必须继承 FloatingActionButton.Behavior 而不是直接继承 CoordinatorLayout.Behavior,因为前者内部保留了原有动画机制,还能复用默认逻辑,避免重复造轮子。
4.6 一个容易踩的坑:CoordinatorLayout 与 NestedScrollView 的滚动冲突
当我使用 NestedScrollView 作为正文容器时,如果里面的内容有横向滑动控件(比如横向的图片画廊),横向滑动时可能会触顶或触底,导致整个页面的纵向滚动失效或卡顿。这时候需要给画廊所在的容器设置 nestedScrollingEnabled 为 false,或者监听它的 touch 事件做拦截。
另一个高频问题是:NestedScrollView 内的内容高度不达屏幕高度时,fillViewport 设置为 false 会造成底部大量空白,页面看起来像没有充满屏幕。要在 ScrollView 上设置 android:fillViewport="true",这样可以让内部内容填充整个视口高度,避免尾巴有一段奇怪的空白区域。
5. 经验总结与进阶建议
这几步做完,你的页面就具备了我们开篇提到的那三件事:AppBarLayout 负责头部折叠、FAB 负责操作入口、正文区负责内容展示,三者通过 CoordinatorLayout 的行为机制形成一条完整的交互链路。这套组合的适用范围非常广,从新闻详情页、商品详情页,到个人主页、搜索落地页,基本都能直接套用。
在这套组合的实际开发中,我个人的体会是:先明确“哪个元素需要响应滚动,哪个元素需要保持固定”,再去做布局选型,比直接照抄示例代码要稳妥得多。AppBarLayout 的滚动标志设计非常灵活,但自由度越高,越容易出问题,尤其要注意不同标志组合对折叠位置的影响。
再补一句经验:如果你追求更精致的滚动效果,可以试试给 AppBarLayout 加上 app:liftOnScroll="true",这样当正文内容滚过工具栏时,工具栏会自动抬升出阴影,瞬间的层级感会有一次肉眼可见的提升。这是 Material 组件库 1.1.0 之后提供的属性,很多项目更新后并没有开启它,白白浪费了官方提供的细节体验。
另外一个值得尝试的玩法是调整 FAB 的大小。默认 fabSize="normal" 是 56dp,如果页面操作不是核心中的核心,可以改成 fabSize="mini",也就是 40dp。在信息密度较高的页面里,mini 尺寸会明显降低按钮的视觉侵略性,让内容占据主视觉。二者配合 AppBar 折叠后的工具栏,能构建出不同的整体气质。
最后,这两个组件的组合还有一条延展路线可以深入:把 AppBarLayout 的折叠状态与 FAB 的图标和颜色进行状态联动。比如完全展开时 FAB 是描边状态的“未收藏”图标,折叠成工具栏后变成实心“已收藏”图标。这个逻辑放在 addOnOffsetChangedListener 里实现即可,本质就是根据 collapseRatio 切换图标资源。
做一个不割裂的滚动联动页面并不难,难的是对细节的把控。希望这篇文章能帮你把 AppBarLayout 和 FloatingActionButton 这套组合理解得更透彻,少走点我当时踩过的弯路。
