1. 折叠效果到底在做什么
1.1 从一个常见界面说起
你肯定见过这种界面:顶部是一张很大的头图或者一块品牌色区域,往下滚动的时候,这块区域会慢慢收缩,最后只留下一条窄窄的标题栏固定在顶部,继续滚动列表内容,那条标题栏纹丝不动。等往上滚回去,头图又慢慢展开恢复原样。
这个交互在移动端非常普遍,个人主页、商品详情页、活动页、音乐播放页,几乎到处都在用。它解决的核心问题是:在有限的屏幕高度里,既要展示有视觉冲击力的大图,又要保证功能性的标题栏和操作入口始终可达。如果头图一直占着位置,用户往下看内容时可用区域就很小;如果头图直接滚走,用户又失去了品牌感和上下文。
传统做法是自己监听滚动事件,手动计算偏移量去改头部的高度或者透明度。这种做法能跑,但问题很多:滚动事件触发频率高,容易掉帧;手势和滚动的边界情况处理起来很烦;和下拉刷新、嵌套滚动一起用的时候经常打架。而CoordinatorLayout加AppBarLayout这套组合,就是官方给出的声明式解决方案——你只需要在布局里描述"谁跟着谁动、怎么动",剩下的协调工作交给它们。
1.2 三个核心角色各干什么
这套方案里主要有三个角色,理解它们的分工是后面所有配置的基础。
CoordinatorLayout是一个增强版的FrameLayout,它的核心能力是协调子 View 之间的依赖关系。普通 ViewGroup 里各个子 View 是各管各的,而 CoordinatorLayout 允许一个子 View 声明"我的行为依赖于另一个子 View",然后通过Behavior机制来响应对方的变化。它本身不产生折叠效果,它是那个"调度中心"。
AppBarLayout是一个垂直方向的LinearLayout,它继承自 LinearLayout,专门用来放顶部的栏。它的关键作用是把内部子 View 的滚动行为暴露出去,让外部的 Behavior 能够感知到"头部该收还是该放"。它自己也不直接做动画,它提供的是滚动的语义。
Behavior是真正干活的。AppBarLayout.ScrollingViewBehavior负责让下面的滚动内容(通常是RecyclerView或NestedScrollView)跟着头部一起协调;AppBarLayout.Behavior负责处理头部的折叠和展开。你写在 XML 里的app:layout_scrollFlags这些属性,最终都是被 Behavior 读取并执行的。
1.3 为什么必须用 NestedScroll 体系
这里有个很多人踩过的坑:如果你在 CoordinatorLayout 里放一个普通的ScrollView或者ListView,会发现折叠效果根本不生效。原因在于,Behavior 之间的协调依赖的是NestedScrolling(嵌套滚动)机制。
简单说,嵌套滚动就是让子 View 在滚动时,先把滚动事件"上报"给父容器,父容器有机会先消费一部分,剩下的再交给子 View 自己处理。AppBarLayout 就是通过这个机制,在列表往上滚的时候先"吃掉"一部分滚动量用来折叠自己。
所以滚动内容必须实现NestedScrollingChild接口。RecyclerView和NestedScrollView天然支持,这也是为什么官方示例里几乎都用这两个。如果你非要用别的控件,就得自己实现嵌套滚动接口,成本很高,不推荐。
提示:判断一个滚动控件能不能配合折叠效果,就看它是不是 NestedScrollingChild。这是硬性前提,不是可选项。
2. 布局结构与关键属性拆解
2.1 最小可用的布局骨架
先看一个能跑起来的最小结构,后面所有变化都是在这个骨架上做加减。
<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="wrap_content" android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_scrollFlags="scroll|enterAlways|snap" app:title="标题" /> </com.google.android.material.appbar.AppBarLayout> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>这个结构里有两个地方是必须的,缺一不可。第一是AppBarLayout内部的子 View 必须设置app:layout_scrollFlags,否则它不会参与折叠。第二是滚动内容必须设置app:layout_behavior="@string/appbar_scrolling_view_behavior",这个字符串常量对应的就是AppBarLayout.ScrollingViewBehavior,它负责把滚动内容和 AppBarLayout 关联起来。
很多人第一次写的时候忘了第二个,结果头能滚但列表位置不对,或者列表被头部盖住。这个 behavior 的作用之一就是自动给滚动内容留出头部的高度偏移,保证内容不被遮挡。
2.2 scrollFlags 五个标志位逐个讲透
layout_scrollFlags是整套方案里最核心的属性,它决定了头部怎么响应滚动。它有五个可选值,可以组合使用,但组合顺序和搭配有讲究。
scroll是最基础的标志。只有加了这个,这个子 View 才会跟着滚动一起移出屏幕。如果不加 scroll,其他标志位都没有意义,这个 View 会一直固定在顶部。所以任何想参与折叠的 View 都必须带 scroll。
enterAlways控制的是"什么时候重新出现"。默认情况下(不加这个),只有当列表滚回顶部时,头部才会重新展开。加上 enterAlways 之后,只要用户往下滚(手指往下滑,内容往上走),头部就立刻开始出现,不用等到滚到顶。这个适合那种希望头部快速回归的场景,比如搜索栏。
enterAlwaysCollapsed必须和 enterAlways 一起用。它的效果是:头部先以"折叠状态"的高度出现,等列表滚到顶部了,再展开到完整高度。这个用在头部很高、希望它先露一小截的场景。
exitUntilCollapsed控制的是"什么时候完全消失"。加了它之后,头部不会完全滚出屏幕,而是收缩到最小高度(通常是 Toolbar 的高度)就停住,继续滚动时头部保持固定。这是实现"粘性头"的关键——头图滚走,但标题栏留下。
snap是让折叠状态"吸附"。不加 snap 的时候,如果你滚到一半松手,头部可能停在半折叠的中间状态,看起来有点尴尬。加上 snap 之后,松手时头部会自动吸附到完全展开或完全折叠两个状态之一,交互更干净。
下面这张表把常见组合和效果对应起来,方便你按场景选:
| 组合 | 效果 | 典型场景 |
|---|---|---|
| scroll | 头部随滚动完全移出 | 简单的可滚动头图 |
| scroll|enterAlways | 下滚立即出现,上滚移出 | 搜索栏、筛选栏 |
| scroll|exitUntilCollapsed | 收缩到最小高度后固定 | 粘性标题栏 |
| scroll|enterAlways|snap | 快速出现且状态吸附 | 需要干脆交互的头部 |
| scroll|exitUntilCollapsed|snap | 收缩固定且吸附 | 大图加粘性标题 |
2.3 最小高度从哪来
"粘性头"能粘住,靠的是 AppBarLayout 内部有一个 View 设置了app:layout_collapseMode="pin",并且这个 View 有明确的高度。折叠时,AppBarLayout 会收缩到所有 pin 住的 View 的总高度。
通常这个 pin 的 View 就是 Toolbar,它的高度是?attr/actionBarSize。所以最终粘在顶部的就是这一条 Toolbar。如果你想让粘住的部分更高一点,可以在 Toolbar 上面再放一个固定高度的 View 并设成 pin,或者直接给 Toolbar 设一个更大的高度。
这里有个容易忽略的点:AppBarLayout 的总高度是所有子 View 高度之和,折叠过程就是把这个总高度压缩到 pin 部分的高度。所以如果你放了头图加 Toolbar,总高度是头图高度加 Toolbar 高度,折叠后剩 Toolbar 高度,中间被压缩掉的就是头图那部分。
3. 折叠模式与视觉细节
3.1 collapseMode 的三种模式
layout_collapseMode决定了子 View 在折叠过程中怎么表现,它有三个值。
pin是固定模式。设成 pin 的 View 在折叠时不会被压缩,它会一直保持原高度,最终成为粘在顶部的那部分。Toolbar 基本都用这个。
parallax是视差模式。设成 parallax 的 View 在折叠时会以不同于滚动速度的速度移动,产生一种"背景移动得慢一点"的层次感。配合layout_collapseParallaxMultiplier可以调整视差系数,取值 0 到 1 之间。系数越大,视差越明显。头图用这个模式会显得更有质感。
none是默认值,不参与折叠的特殊处理,跟着一起被压缩。
一个典型的头图加标题栏的配置是这样的:
<com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:layout_width="match_parent" android:layout_height="200dp" android:scaleType="centerCrop" android:src="@drawable/header" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.5" app:layout_scrollFlags="scroll|exitUntilCollapsed" /> <com.google.android.material.appbar.MaterialToolbar android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:layout_scrollFlags="scroll|exitUntilCollapsed" /> </com.google.android.material.appbar.AppBarLayout>注意这里两个子 View 都带了scroll|exitUntilCollapsed。头图带 scroll 才能滚走,带 exitUntilCollapsed 才能让整体收缩到 Toolbar 高度而不是完全消失。Toolbar 带 scroll 是为了参与整体的折叠计算,带 pin 是为了最终粘住。
3.2 视差系数的实际观感
视差系数这个东西,光看文档很难有概念,得实际调。我的经验是:头图高度在 180dp 到 240dp 之间时,系数取 0.5 左右比较舒服。系数太小(比如 0.2),视差几乎看不出来,跟没有一样;系数太大(比如 0.9),头图移动速度接近甚至超过滚动速度,会有一种"图片在追着手指跑"的怪异感。
如果你用的是CollapsingToolbarLayout,它内部对头图的处理会更细腻一些,因为它还涉及标题文字的缩放和淡出。这个后面单独说。
3.3 状态栏与沉浸式的配合
折叠效果经常和沉浸式状态栏一起用,让头图延伸到状态栏下面,视觉上更完整。做法是给 AppBarLayout 加android:fitsSystemWindows="true",同时 Activity 里设置状态栏透明。
但这里有个坑:fitsSystemWindows加在 AppBarLayout 上之后,AppBarLayout 会自动给内容加上状态栏高度的 padding。如果你同时又在代码里手动处理状态栏,就会重复加,头部会莫名多出一块空白。我的建议是二选一,要么全交给 fitsSystemWindows,要么全手动处理,别混着来。
另外,如果头图要延伸到状态栏,ImageView 的scaleType建议用centerCrop,并且给它加一个和状态栏等高的顶部 padding 或者用android:paddingTop配合负 margin,具体看你的视觉需求。这块没有标准答案,多试几次找到合适的值就行。
4. 从零搭一个完整可跑的示例
4.1 依赖与主题准备
这套组件来自 Material Components 库,先确认依赖里有它:
implementation 'com.google.android.material:material:1.11.0'版本号按你项目实际情况来,但建议不要用太老的版本,老版本里 AppBarLayout 的一些边界 bug 比较多。主题方面,Activity 需要继承 Material 主题,否则 Toolbar 的样式和状态栏处理可能不对:
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> <item name="colorPrimary">@color/primary</item> <item name="colorPrimaryVariant">@color/primary_dark</item> <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:windowLightStatusBar">false</item> </style>NoActionBar是必须的,因为我们要用自己的 Toolbar。状态栏设成透明是为了让头图能延伸上去。
4.2 完整布局文件
把前面的骨架和头图组合起来,得到一个完整的布局:
<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="wrap_content" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"> <com.google.android.material.appbar.CollapsingToolbarLayout android:layout_width="match_parent" android:layout_height="240dp" app:contentScrim="?attr/colorPrimary" app:expandedTitleMarginStart="16dp" app:expandedTitleMarginBottom="24dp" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:title="详情页"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" android:src="@drawable/header" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.5" /> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:navigationIcon="@drawable/ic_back" /> </com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.AppBarLayout> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>这里引入了CollapsingToolbarLayout,它比裸用 AppBarLayout 多做了几件事:标题文字会随着折叠从大字号缩到小字号并移动到 Toolbar 位置;折叠到一定程度时会给背景加一层contentScrim颜色,保证标题可读;还能控制展开状态下标题的边距。
4.3 代码里绑定数据
布局搭好之后,代码部分其实很轻,主要是给 RecyclerView 设适配器,以及处理 Toolbar 的返回按钮:
class DetailActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_detail) val toolbar = findViewById<MaterialToolbar>(R.id.toolbar) setSupportActionBar(toolbar) toolbar.setNavigationOnClickListener { finish() } val recycler = findViewById<RecyclerView>(R.id.recycler) recycler.layoutManager = LinearLayoutManager(this) recycler.adapter = MyAdapter(dataList) } }注意setSupportActionBar之后,标题会由 ActionBar 接管,而 CollapsingToolbarLayout 的标题是通过app:title设置的,两者可能冲突。如果你用 CollapsingToolbarLayout 的标题,就不要在代码里再调supportActionBar?.title = ...,否则会出现两个标题或者标题不缩放的问题。我的做法是统一用 CollapsingToolbarLayout 的title属性,Toolbar 只负责放返回按钮和菜单。
4.4 参数选择的计算过程
头图高度设多少合适?这不是拍脑袋定的。假设屏幕高度是 640dp(约 5 寸屏),状态栏 24dp,Toolbar 56dp。如果头图设 240dp,那么展开状态下头部总高是 240dp,占屏幕约 37.5%。折叠后剩 56dp 加状态栏,内容区从 320dp 变成 560dp,多出 240dp 的可视空间。这个比例在大多数手机上观感是舒服的。
如果头图设得太高,比如 320dp,展开时内容区只剩 240dp,用户第一眼几乎看不到内容,体验不好。如果设得太矮,比如 120dp,折叠前后差别不大,折叠效果就失去了意义。所以 200dp 到 260dp 是一个比较稳妥的区间,具体根据你的视觉稿微调。
视差系数 0.5 的含义是:头图移动速度是滚动速度的一半。滚动 240dp 时头图只移动 120dp,剩下 120dp 是被压缩掉的。这样头图在折叠过程中始终有一部分可见,不会突然消失。
5. 常见问题与排查实录
5.1 折叠不生效的几种原因
这是问得最多的问题。折叠完全不生效,按下面顺序排查:
第一,滚动控件是不是 NestedScrollingChild。用 ScrollView 或 ListView 的,换成 NestedScrollView 或 RecyclerView。这是最常见的原因。
第二,滚动控件有没有设app:layout_behavior="@string/appbar_scrolling_view_behavior"。漏了这个,滚动内容和 AppBarLayout 就没有关联,头部不会响应。
第三,AppBarLayout 的子 View 有没有设layout_scrollFlags。没设 scroll 标志的 View 不参与折叠。
第四,AppBarLayout 的高度是不是wrap_content。如果写死成某个高度,折叠计算会出问题。必须是 wrap_content。
第五,CoordinatorLayout 是不是根布局。如果 AppBarLayout 和滚动内容不在同一个 CoordinatorLayout 下,协调机制不工作。
5.2 列表顶部被头部遮挡
现象是列表第一项被 Toolbar 盖住一部分。原因是滚动内容没有正确获得头部高度的偏移。正常情况下 ScrollingViewBehavior 会自动处理这个偏移,但如果你的布局层级不对,或者手动改了 behavior,偏移就会丢。
排查方法:确认滚动内容直接是 CoordinatorLayout 的子 View,且 behavior 设置正确。如果滚动内容外面还包了一层 FrameLayout 之类的容器,behavior 就作用不到真正的滚动控件上,偏移自然不对。这种情况要么去掉中间容器,要么把 behavior 设到中间容器上并确保它能传递嵌套滚动。
5.3 快速滑动时头部抖动
快速 fling 的时候头部一顿一顿的,或者折叠到一半卡住。这通常是 snap 标志和 fling 速度配合的问题。加上 snap 之后,Behavior 会在松手时根据当前状态和速度决定吸附方向,一般能解决抖动。
如果加了 snap 还抖,检查一下是不是在 AppBarLayout 上做了自定义的 offset 监听并在里面改了布局,这种手动干预容易和 Behavior 自己的动画打架。我的建议是尽量别在滚动过程中手动改 AppBarLayout 的属性,交给它自己管。
5.4 和下拉刷新冲突
下拉刷新控件(比如 SwipeRefreshLayout)和折叠效果一起用的时候,经常出现下拉时头部不展开、或者刷新指示器位置不对。原因是 SwipeRefreshLayout 自己也要消费嵌套滚动,和 AppBarLayout 抢事件。
解决办法是把 SwipeRefreshLayout 放在 CoordinatorLayout 里,作为滚动内容的父容器,并且确保它的嵌套滚动配置正确。有些版本的 SwipeRefreshLayout 需要手动调setNestedScrollingEnabled(true)。如果还是冲突,可以考虑用 Material 库自带的刷新组件,它和 CoordinatorLayout 的配合更顺。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 完全不折叠 | 滚动控件非 NestedScrollingChild | 换 RecyclerView/NestedScrollView |
| 头部不响应 | 缺 layout_behavior | 补上 appbar_scrolling_view_behavior |
| 子 View 不折叠 | 缺 scrollFlags | 加 scroll 标志 |
| 列表被遮挡 | behavior 未生效 | 检查布局层级 |
| 快速滑动抖动 | 缺 snap | 加 snap 标志 |
| 下拉刷新冲突 | 嵌套滚动抢占 | 调整容器层级 |
| 标题不缩放 | 用了 ActionBar 标题 | 统一用 CollapsingToolbarLayout 标题 |
5.6 几个我踩过的坑
第一个坑是fitsSystemWindows重复设置。我在 Activity 和 AppBarLayout 上都设了,结果头部多出一块状态栏高度的空白,查了半天才发现是重复了。记住只在一处设。
第二个坑是 CollapsingToolbarLayout 的高度用了 wrap_content。它必须是一个确定的高度,否则折叠计算没有基准,效果会乱。我一般直接写死一个 dp 值。
第三个坑是在 RecyclerView 的 item 里又放了可横向滚动的东西,结果纵向折叠和横向滚动偶尔会串。这种情况要在横向滚动控件上处理好嵌套滚动,或者干脆避免这种嵌套。
第四个坑是给 AppBarLayout 加了android:background之后,折叠时背景不跟着变,露出下面的内容。正确做法是用 CollapsingToolbarLayout 的contentScrim来控制折叠后的背景色,而不是直接给 AppBarLayout 设背景。
6. 进阶玩法与扩展思路
6.1 监听折叠状态做联动
有时候需要在折叠到某个程度时做点别的事,比如改变状态栏图标的颜色、显示或隐藏某个悬浮按钮。可以通过给 AppBarLayout 加OnOffsetChangedListener来实现:
appbar.addOnOffsetChangedListener { _, verticalOffset -> val totalScrollRange = appbar.totalScrollRange val collapsed = verticalOffset == 0 val collapsedFraction = if (totalScrollRange != 0) { -verticalOffset.toFloat() / totalScrollRange } else 0f if (collapsedFraction > 0.8f) { // 接近完全折叠,切换状态栏图标为深色 } else { // 展开状态,状态栏图标为浅色 } }verticalOffset是负值,0 表示完全展开,等于-totalScrollRange表示完全折叠。用这个比例可以做很多联动效果,比如让某个 View 的透明度跟着折叠比例变化。
6.2 多段折叠
如果头部有多层,比如一个大图加一个搜索栏加一个 Tab,想让它们分阶段折叠,可以给不同的子 View 设置不同的 scrollFlags。比如大图用scroll|exitUntilCollapsed,搜索栏用scroll|enterAlways,Tab 用scroll|enterAlways|snap。这样滚动时大图先收,然后搜索栏和 Tab 按各自的规则响应,形成层次感。
不过多段折叠的调试成本比较高,各个标志位的组合效果需要反复试。我的建议是先用最简单的单段折叠跑通,再逐步加复杂度,每加一层就测一遍,别一次性堆上去。
6.3 和 ViewPager2 的配合
Tab 加 ViewPager2 是很常见的组合。把 TabLayout 放在 AppBarLayout 里,ViewPager2 作为滚动内容。这里要注意的是,ViewPager2 内部的每个页面如果是可滚动的,嵌套滚动会多一层。通常的做法是让 ViewPager2 本身不滚动(设成固定高度或者用 NestedScrollView 包),由内部页面自己处理滚动,这样折叠效果才能正确传递。
如果发现滑动 ViewPager2 时头部不折叠,检查一下 ViewPager2 的嵌套滚动有没有被禁用,以及内部页面的滚动控件是不是 NestedScrollingChild。
6.4 性能上的注意点
折叠效果本身性能开销不大,因为它是基于嵌套滚动的,不是每帧都重新测量布局。但有几个地方容易拖慢:
头图如果是一张大图,解码和绘制会有开销,建议按实际显示尺寸做采样,别直接加载原图。视差效果会让头图在折叠过程中持续重绘,如果头图上有复杂的叠加层,考虑用硬件加速或者简化。
另外,OnOffsetChangedListener里别做耗时操作,它会在滚动过程中频繁回调。如果要在里面改 View 属性,尽量做轻量级的,比如改透明度、改颜色,别做布局相关的操作。
6.5 一个容易被忽略的细节
AppBarLayout 的折叠是基于它自身高度的,如果它的高度是 wrap_content 且内部有动态变化的 View,折叠范围会跟着变。这在大多数情况下没问题,但如果你在运行时动态往 AppBarLayout 里加 View,记得重新触发布局,否则折叠范围可能不对。
还有一个细节是,exitUntilCollapsed和enterAlways一起用的时候,行为可能和你预期的不一样。enterAlways 会让头部在下滚时立即出现,但 exitUntilCollapsed 又要求它收缩到最小高度就停。这两个一起用时,头部会先以最小高度出现,然后随着继续下滚展开。如果你想要的是"下滚立即完整展开",那就不该用 exitUntilCollapsed,而应该用 enterAlways 加 enterAlwaysCollapsed 的组合。这个区别很微妙,实际调的时候多试几种组合就明白了。
我个人在实际项目里的体会是,折叠效果这东西,布局结构对了就成功了一大半,剩下的就是标志位和参数的微调。别一上来就追求复杂效果,先把最简单的 scroll 加 exitUntilCollapsed 跑通,看到头部能收缩能粘住,再往上加视差、加标题缩放、加联动。每加一个特性就单独测一遍,出问题也好定位。另外,不同 Android 版本和不同厂商的 ROM 在嵌套滚动的实现上偶尔有差异,如果遇到只在某些机型上出现的怪异行为,优先怀疑是不是 ROM 的滚动实现有改动,而不是你的代码写错了。