别靠肉眼:用 ffmpeg 抽帧拼图客观验证 UI 动画
给 App 做 AI 聊天功能时,遇到一个问题:键盘弹出的瞬间,底部输入栏明显闪动。加了
adjustResize后我「觉得」好了——但动画就几百毫秒,到底是真平滑了,还是我看顺眼了?这篇讲怎么用一段录屏 +ffmpeg抽帧拼图,把「流不流畅」从主观感受变成看得见的客观证据。
一个说不清的「闪动」
场景是一个 edge-to-edge 的聊天页,底部是输入栏。点输入框、键盘弹出的那一刻,输入栏会闪一下——位置跳、还有一下发黑。
问题不在于改不改得动,而在于怎么确认改好了:
- 动画总共两三百毫秒,肉眼只能捕捉到「好像抖了一下」
- 改一版、再看一眼,「这次好像顺了」——但这是改好了,还是我看习惯了?
- 没法复现、没法量化、更没法和别人说清「就是这一帧不对」
UI 动画的 bug 特别容易陷进这种主观泥潭。我需要一个能定格、能回放、能对比的客观依据。
先说那个怀疑的修复:adjustResize
根因其实不复杂。开了 edge-to-edge(enableEdgeToEdge)后,窗口默认可能走 pan 模式——键盘弹出时系统平移整个窗口;而 Compose 这边又用 imePadding() 给输入栏做了 inset 动画。两套机制同时作用、互相打架,过渡帧里就出现了位置跳变和未绘制的黑块。
修法是给 Activity 显式声明 adjustResize,让 IME 以动画 inset 的形式平滑下发,不再 pan:
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize" />
改完一行,我「觉得」好了。但改完不能拍脑袋说好了——尤其是动画这种主观的东西。下面才是这篇的主角。
把动画「定格」:录屏 + 抽帧拼图 ⭐
思路很简单:把时间轴铺平成空间。录一段交互过程,按固定帧率抽出动画那段的每一帧,平铺成一张图——整条动画轨迹一眼看完,哪一帧不对一目了然。
两条命令:
# 1. 录制交互过程(最多 3 秒),触发动画后等它录完再 pull
adb shell screenrecord --time-limit 3 /sdcard/rec.mp4
adb pull /sdcard/rec.mp4 .
# 2. 归一帧率 → 截动画时间窗 → 缩放 → 平铺成一张图
ffmpeg -i rec.mp4 \
-vf "fps=30,select='between(n\,15\,42)',scale=200:-1,tile=7x4" \
-frames:v 1 montage.png
-vf 这串滤镜链是关键,拆开看:
fps=30——必须放第一步。这里踩过一个坑:adb screenrecord是可变帧率的,画面变化剧烈(闪动)时帧率高、平稳时帧率低。直接按帧号抽帧,两段录像的「第 20 帧」根本不是同一时刻,没法比。先fps=30重采样到固定帧率,「帧号 = 固定时间」才成立。select='between(n,15,42)'——只取动画发生的时间窗(30fps 下第 15~42 帧 ≈ 0.5s–1.4s),避开前后静止段。scale=200:-1——每帧缩小,拼图才不会过大。tile=7x4——28 帧铺成 7 列 4 行,按时间从左到右、从上到下排列。
为了让对比公平,修复前后用完全相同的参数各录一段、各拼一张。
怎么读这张图
判据很直接:
- 平滑 = 输入栏单调上移、始终贴在键盘上方,相邻帧之间位移均匀
- 闪动 = 出现跳到异常位置、重影、或未绘制的黑块的帧
先看修复前——键盘上来的过程中,中途蹦出一块黑色区域,这就是肉眼只能感觉到「抖了一下」的真身:

再看修复后,相同时间窗、相同拼图参数。键盘平滑滑入,输入栏始终贴着键盘顶部一起上移,没有任何黑块或跳变:

两张图一摆,「修好了没」不再需要争论。一个技巧:如果差异藏在局部,给 ffmpeg 加一段 crop 只截关键区域再放大,跳变会更刺眼。
为什么这招好用
- 客观——把转瞬即逝的动画变成静态图,不再靠「我觉得」
- 可复现——同一套命令任何人都能复跑,参数即口径
- 可对比——修复前后、方案 A/B 并排,差异自己跳出来
- 可归档——直接贴进 PR / issue,团队对齐「就是这一帧」
- 通用——键盘、转场、手势、列表动画……任何动画都能用
- 零成本——纯命令行,
adb+ffmpeg,不装任何额外工具
肉眼调动画总会陷进主观泥潭。给它一把「看得见」的客观标尺,很多说不清的体验问题,瞬间就有了答案。
这篇是和 Claude Code 一起完成的:它驱动
adb/ffmpeg跑完整条录屏抽帧流程,也是它在拼图里先发现了那帧黑块。验证方法本身,也是协作里磨出来的。