【听见课堂 HarmonyOS NEXT 实战系列 29】字幕自动滚动也要可控:暂停跟随与“回到最新”
【听见课堂 HarmonyOS NEXT 实战系列 29】字幕自动滚动也要可控:暂停跟随与“回到最新”
实时字幕默认跟随最新内容很自然,但如果用户正在回看一分钟前没听清的句子,系统仍不断把列表拉到底部,就会让“可回看”变成一句空话。自动滚动不是单纯调用一次 scrollToIndex(),而是一套包含跟随开关、触发条件、空态、恢复动作和无障碍反馈的交互状态。
听见课堂使用 liveAutoScrollEnabled、liveCaptionScroller 和“回到最新”按钮把跟随行为显式化。本文拆解当前实现已经解决的问题,也指出手势滚动尚未自动关闭跟随的边界。

一、自动滚动首先是一个用户状态
页面定义了独立状态:
@Local private liveAutoScrollEnabled: boolean = true;
private liveCaptionScroller: Scroller = new Scroller();
Scroller 负责位置,布尔值负责意图。两者不能混为一谈:列表当前恰好在底部,不代表用户愿意持续跟随;用户关闭跟随,也不代表必须立即跳到某个位置。
二、为什么默认开启
实时课堂的主要任务是及时看到最新一句,默认开启可以减少手工滚动。新用户不需要理解列表状态机,就能从第一条字幕一直跟随到最新。
但“默认开启”不是“永久强制”。当用户需要校对旧内容、查看重点或比对板书时,必须有明显入口停止自动定位。
三、快照更新时不能每次都滚动
applyLiveSnapshot() 会频繁执行:计时器更新、状态文案变化、字幕 partial 更新、人工标记都可能发快照。如果每次都调用滚动,页面会产生抖动,也会不断打断回看。
当前实现只在两类条件发生时触发:
const enteredListening = previousState !== LiveSessionState.LISTENING &&
snapshot.state === LiveSessionState.LISTENING;
if (this.liveAutoScrollEnabled &&
this.currentPage === RouteId.LIVE_CLASS &&
(snapshot.captions.length > previousCaptionCount || enteredListening)) {
setTimeout(() => this.scrollLiveCaptionsToLatest(false), 0);
}
即新增字幕,或者刚进入监听态。纯计时与普通状态刷新不会持续抢夺滚动位置。
四、为什么要检查当前路由
Service 可能在页面切走前后仍发出快照。如果当前页面已经不是实时课堂,继续操作 liveCaptionScroller 没有用户价值,还可能引发不可见组件的时序问题。
this.currentPage === RouteId.LIVE_CLASS 让滚动动作只在列表真正可见时发生。业务状态可以继续同步,视图副作用则受页面可见性约束。
五、为什么使用 setTimeout(…, 0)
快照赋值后,ArkUI 需要先根据新数组构建新的 ListItem。如果同步调用 scrollToIndex(lastIndex),目标节点可能还没有进入布局。
零延时任务把滚动放到下一轮执行,让列表有机会完成更新。这不是为了“延迟动画”,而是处理状态更新和视图布局之间的时序。
六、scrollToIndex 的三个关键参数
实际滚动逻辑是:
this.liveCaptionScroller.scrollToIndex(
this.liveCaptions.length - 1,
true,
ScrollAlign.END
);
最后一个索引定位最新字幕,true 启用平滑动画,ScrollAlign.END 让目标靠近列表尾部,保留前文上下文。若只使用默认对齐,最新卡片可能出现在顶部,视觉上反而难以理解它与前一条的连续关系。
七、空列表必须有单独处理
“回到最新”在没有字幕时不会传入 -1:
if (this.liveCaptions.length === 0) {
if (showFeedback) {
this.liveFeedback = '当前还没有字幕。';
}
return;
}
这既防止非法索引,也给用户一个明确解释。空态不是无事发生,而是一个可沟通的产品状态。
八、暂停跟随为何必须有可见文案
切换按钮直接展示“自动滚动已开启/已暂停”,并同步反馈:
this.liveAutoScrollEnabled = !this.liveAutoScrollEnabled;
this.liveFeedback = this.liveAutoScrollEnabled ?
'字幕自动滚动已开启。' :
'字幕自动滚动已暂停,可自由回看。';
只换一个小图标会让用户不确定当前状态。明确文案还能被无障碍服务朗读,降低状态只靠颜色表达的问题。

九、“回到最新”为什么不只是滚动按钮
用户主动点击“回到最新”时,页面除了滚动,还会:
this.liveAutoScrollEnabled = true;
this.liveFeedback = '已回到最新字幕,并恢复自动滚动。';
这是一项组合动作:恢复空间位置,也恢复后续跟随策略。否则用户刚回到底部,下一条字幕到来时列表仍停住,会让按钮行为显得不完整。
十、关闭跟随后新字幕仍然要写入
liveAutoScrollEnabled 只控制滚动副作用,不控制 liveCaptions 更新。关闭后,新字幕依然由 Service 生成、持久化计数仍然增长,用户稍后点击“回到最新”即可看到。
这条分层非常重要:视图跟随偏好不能改变采集事实。若关闭滚动顺便暂停识别,就会让一个阅读操作意外影响数据链路。
十一、手机和大屏共用同一行为
手机沉浸页和大屏工作区都调用 liveCaptionScrollControls() 与同一个 Scroller。布局不同,但状态含义、按钮文案、空态和恢复逻辑一致。
共享行为可以避免出现“大屏能暂停跟随,手机不能”或同一按钮在两端语义不同的问题。设备适配应该改变空间组织,而不是随意改变核心操作契约。
十二、当前实现的一个重要缺口
当前源码提供显式按钮切换自动滚动,但没有看到 List 的 onScroll 或手势回调在用户向上滑动时自动把 liveAutoScrollEnabled 设为 false。
因此用户若只用手势向上回看、没有先点击“自动滚动已开启”,下一条新字幕到达时仍可能被拉回底部。文章必须把这一点列为待补齐交互,不能因为有两个按钮就宣称所有回看场景已完成。
十三、更完整的手势策略怎么设计
可采用“距底部阈值 + 用户来源”策略:
- 用户主动向上滚动且离底部超过阈值,自动关闭跟随;
- 接近底部但未点击恢复时,仅显示“有新字幕”提示;
- 点击“回到最新”后恢复跟随;
- 程序触发的滚动不要再次误判为用户手势;
- 页面切换与旋转后保留用户选择。
阈值、滚动事件和 ArkUI 当前 API 要以项目 SDK 实测为准,不能直接套用 Web 的 scrollTop 方案。
十四、长课堂还要考虑列表窗口
Service 当前最多保留 MAX_VISIBLE_CAPTIONS 条实时字幕,超出后会切片保留尾部。这样可以控制页面重建与内存压力,但也意味着实时列表不是无限历史。
用户需要更早内容时,应从已经持久化的历史/复盘页面查询,而不是把几小时全部字幕永久堆在同一个 List 中。滚动体验与数据分页要共同设计。
十五、验收矩阵
至少验证:
| 场景 | 期望 |
|---|---|
| 首条字幕到达 | 默认滚到最新 |
| 只更新计时 | 不改变列表位置 |
| 关闭自动滚动后新增字幕 | 数据增加,位置不被抢走 |
| 空列表点击回到最新 | 提示当前没有字幕,不传非法索引 |
| 点击回到最新 | 滚至末尾并恢复自动跟随 |
| 手机/大屏切换 | 控件与语义一致,列表仍可滚动 |
| 大字号、长文本 | 最后卡片完整可见,不遮挡底部控制区 |
还应补测手势回看后的新字幕行为,因为这正是当前代码没有自动关闭跟随的缺口。
十六、总结
自动滚动的本质不是“每来一条就拉到底”,而是尊重用户当前阅读意图。听见课堂已经把跟随开关、触发条件、空态和恢复动作显式化;下一步仍需把用户手势纳入状态机,让向上回看能够自然暂停跟随。
下一篇将从同一实时课堂继续扩展:手机沉浸页与 2in1 大屏工作区如何共享 Service、状态和控件语义,同时采用不同的空间布局。
参考:项目当前 Index.ets 中 applyLiveSnapshot()、scrollLiveCaptionsToLatest()、liveCaptionScrollControls() 与两套实时课堂 Builder。
更多推荐

所有评论(0)