上周我在一个扫脸测量页面里,碰到一个视觉体验上的小问题:一条沿扫脸可视区域椭圆外圈走 30 秒的紫色进度条,功能没问题,但是视觉上却是一卡一卡,我一开始以为,是动画参数没调好,最后把问题定位准确以后,我才发现,实际上真正的问题不是动画本身,而是进度更新得太慢,紫线和气泡显示出来的时机也没对齐。
上周我在一个扫脸测量页上,花了 5 到 6 个小时,只为修复一条沿着测量视窗外圈走一圈的紫色进度条的视觉体验问题,如图所示,我让 AI 进行分析,然后出了4版方案都没有把问题解决。
这看起来是个很小的问题,但产品经理给我的反馈很具体:
- 这条线会沿着外圈顺时针转一整圈
- 30 秒走完
- 路径是对的
- 时长也是对的
- 但肉眼看起来就是一卡一卡,像跳着走完了一圈
如果路径错了,就去改路径。
如果时长错了,就去改时长。
如果进度值错了,就去查状态。
但这些都没错,功能是对的,路径是对的,30 秒总时长也是对的,可用户一眼看过去,就会觉得一卡一卡的。
就在今晚,我让 AI 重新仔细分析代码之后,出了第5版方案,才把这个问题真正解决,实现了丝滑加载的效果。
这次反复尝试修这个问题的过程,我觉得很值得单独写下来复盘一下。
我最早定位错误的,就是动画参数
第一眼看到这种现象,太容易往“动画不够顺畅”上去想了。
一条线沿固定路径往前走,看起来不够丝滑顺畅,这不是动画问题,还能是什么呢?
所以我最早看的也是这些方向:
strokeEnd每次更新的步子是不是太大- 动画时长是不是不对
- 要不要补
CABasicAnimation - 要不要直接上
CADisplayLink
这条思路并不奇怪。换谁来,第一反应可能都差不多,但我往下看了一会儿就发现,自己其实把问题想得太简单了。
你在屏幕上看到的是一条线,可这条线背后的功能逻辑,至少包含了三件事:
- 业务层到底怎么算进度
- 页面多久把这个进度刷新一次
- 手机屏幕上的进度线是立刻到位,还是会自己再补一小段
这三件事只要有一件逻辑处理不对,最后用户看到的,就是一卡一卡的。
更关键的是,这还是扫脸测量页,本来就属于核心功能页面,所以,我从一开始就不想为了修复一个问题,影响到测量状态、暂停恢复、扫脸结果生成等核心功能的逻辑。
于是,我在开始修改代码之前,就跟 AI 定下了一条边界:只改视觉体验效果,绝对不动扫脸核心功能逻辑。
第一层真正的问题,其实是那条 250ms 的低频刷新
我仔细查看代码的时候,才发现,外圈这条紫线不是自己单独更新的,页面上的提示文案、秒数显示这些内容,会和它一起更新,而它们 250ms 才统一刷新一次:
private enum RenderThrottle {
static let panelIntervalMs = 250
}
250ms 是什么概念?
差不多就是 4fps。
你让一个用户一直盯着看的进度圈只按 4fps 往前跳,哪怕逻辑完全正确,肉眼也一定会觉得它是一卡一卡的。
所以我第一步的判断很简单:
- 不碰原来那套进度值怎么计算
- 不碰扫脸测量本身怎么跑
- 先把外圈进度从这条
250ms的低频刷新里拆出来
这一步做完以后,效果确实比之前好了,至少不是那种很明显的一卡一卡跳着加载进度了。
但问题并没有解决,真机上看,它还是没有那种丝滑顺畅往前走的感觉。
事情到这里,说明这个体验问题我并没有定位准确。
真正让我重新看清楚问题的,不是代码,而是那个30s 的小气泡。
后面几版方案之所以总是“有改善,但还不够”,关键就在于我一直把这个问题想成:
只要把那条紫线调得丝滑顺畅就行了。
可用户看到的,从来不只是那条紫线。
跟着轨道一起走的,还有那个 30s 的气泡。
这下子很多之前解释不通的现象都通了。
如果我只盯着紫线怎么更丝滑顺畅,而那个气泡还在按低频节奏一段一段跳,那用户最后看到的,还是一卡一卡的效果。
也就是到这时候,我才把问题看清楚:
我不只是在修改那条紫线,我还要处理整套画面元素怎么一起同步往前走。
这时候该拆出去的,不只是紫线本身,而是:
- 紫线
- 秒数气泡
- 它们两个显示到屏幕上的节奏
它已经不是“动画参数怎么调顺一点”,它变成了:
一套本来应该同步往前走的画面元素,为什么没有按同一个节奏显示出来。
最后把问题解决掉的,不是动画曲线,而是刷新方式
第5版的解决方案,改动其实很小。
我没有去动:
- 扫脸测量核心功能
- 30 秒总时长
- 扫脸暂停以后怎么继续测量
- 扫脸识别本身
- 扫脸质量判断本身
- 扫脸结果生成和历史记录
我改的只有一件事:怎么把现有的进度值显示出来。
最后的做法其实就是这几步:
- 原来那套进度值继续照常算
- 页面上单独加一条更新更快的显示进度
- 紫线和秒数气泡一起跟着这条显示进度往前走
- 其他提示文案继续按原来的慢节奏更新
这一步做完以后,外圈总算实现“丝滑顺畅转圈”的视觉体验了,但事情还没完全结束。
真机上后来又冒出来一个很小的细节问题:
紫线在转的时候,没有紧贴着气泡走,看起来总像是慢了半拍。可一停下来,它又重新贴上了。
我一开始怀疑:
- 是不是紫线的路径又有问题
- 是不是位置没对齐
- 是不是拿到的进度值不一样
后面重新看代码,问题比这些都小:
- 气泡的位置是立即更新的
- 紫线的
strokeEnd在高频更新里还有一点系统默认动画,或者显示到屏幕上时慢了半拍
它们两个虽然用的是同一个进度值,但显示到屏幕上的节奏并不完全一致。
气泡已经到了,紫线还在追。
所以进度加载的时候紫线没有紧贴气泡,停下来又会重新贴上。
最后把这个细节解决掉的,不是什么大重构,只是很小的一步:
- 显式关掉
progressLayer.strokeEnd的默认动画 - 让紫线和气泡都按同一帧、同一种“立即到位”的方式更新
到这里,这个问题才算全部解决。
这次最值得记住的,是我怎么一步步把问题看清楚。
如果只看最后那几行代码,这次的问题不大。
真正花时间的,不是写出那几行,而是前面一路把我对问题定位的判断推翻。
这次对我最大的提醒,是以下三件事情。
第一,功能对了,不代表看起来的感觉就对了。
很多 UI 问题最容易误导人的地方,就是表面上所有东西都对:
- 进度值对
- 路径对
- 总时长对
可用户视觉感受就是不对,很多时候,不是进度算错了,而是页面最后显示出来的样子还是不丝滑顺畅。
第二,很多看起来像动画的问题,实际上有可能是更新方式和显示节奏的问题。
一开始太容易去调整动画了:
- 时长
- 曲线
strokeEndCABasicAnimation
但这次真正影响视觉感受的,是更新频率太低,是进度一段一段往前跳,也是一整套画面元素没有按同一个节奏显示出来。
第三,在关键页面里修复体验问题,一定要注意好修复问题的边界和风险。
如果这次为了修复一个视觉体验问题,影响到扫脸测量状态、扫脸暂停恢复、扫脸结果页怎么生成等等,那就得不偿失了。
回头看,这次最后能够把问题彻底修复,不是用了什么特别高级的办法。
更关键的是,我在修复问题的过程中,提前把边界和风险确定好了。