指纹解锁动画空跑不停止 — 问题排查

日期: 2026-05-08
Trace: /home/zbc/pangu/GeneralAndroid/main/hideapi/tool_new/output/trace/perfetto_1778220614.pftrace
曲线图: captures/animation_curve_full.png


一、现象

指纹解锁时,folme 动画 magazine_translateY 到达目标值后不停止,持续空跑约 1100ms(66帧),每帧值完全不变。

Trace 中对应的动画信息:

Folme 1327 all_in_one_lock_screen Value{valid ValueTargetObject@1768817721{all_in_one_clock}}
  magazine_translateY,font_weight,translationY,notification_top,width,height
  spring_phy(1,0.18,1)

动画数据摘要:

指标
起始值-326.1008
目标值-304.1231079102
总位移~22 pixels
有效运动时间942ms (66 frames)
空跑时间~1100ms (66 frames)
总时长~2040ms (132 frames)
浪费帧占比50%
曲线类型过阻尼弹簧,无overshoot

二、动画曲线

animation_curve_full

  • 上图: 完整曲线,红色区域为空跑帧(值不变)
  • 中图: 有效运动阶段放大,前 200ms 完成 90% 位移,后 740ms 收敛最后 10%
  • 下图: 速度曲线,峰值 200 units/sec,之后指数衰减

三、根因分析

3.1 问题代码

文件: packages/SystemUI/miuiModules/Keyguard/src/main/java/com/android/keyguard/clock/animation/allinone/AllInOneClockAnimation.kt:455

private val MARGAZINE_TRANSLATEY: ValueProperty<Int> = ValueProperty("magazine_translateY", 0f)
//                                                                                         ^^^
//                                                                     minVisibleChange = 0f  ← BUG

对比其他属性:

private val FONT_WEIGHT: ValueProperty<Int> = ValueProperty("font_weight", 1f)       // 正确
private val NOTIFICATION_TOP: ValueProperty<Float> = ValueProperty("notification_top", 1f) // 正确

3.2 Folme 收敛判断逻辑

文件: miuix/library/folme/src/.../physics/EquilibriumChecker.java

// Line 39-42: 阈值计算
public void init(FloatProperty property, double targetValue) {
    mValueThreshold = property.getMinVisibleChange() * THRESHOLD_MULTIPLIER;      // 0f * 0.75 = 0
    mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER;         // 0 * 16.67 = 0
    mTargetValue = targetValue;
}
 
// Line 45-49: 收敛判断
public boolean isAtEquilibrium(int easeStyle, double value, double velocity) {
    boolean isAt = isAt(value, mTargetValue);  // Math.abs(value - target) < 0  → 永远 false
    return (easeStyle != EaseManager.EaseStyleDef.SPRING_PHY || isAt)
            && easeStyle != EaseManager.EaseStyleDef.ACCELERATE
            && Math.abs(velocity) < mVelocityThreshold;  // velocity < 0  → 永远 false
}

3.3 为什么 minVisibleChange = 0 导致不停止

minVisibleChange = 0f 时:

  1. mValueThreshold = 0f * 0.75f = 0f
  2. mVelocityThreshold = 0f * (1000/60) = 0f
  3. isAt() 要求 Math.abs(value - target) < 0f数学上永远不成立(除非完全相等)
  4. 速度判断要求 Math.abs(velocity) < 0f同样永远不成立

弹簧物理模拟中,由于浮点精度问题,value 和 velocity 几乎不可能精确等于 0,导致动画永远无法通过收敛检查。

3.4 为什么最终还是停了

文件: miuix/library/folme/src/.../internal/FolmeCore.java:28, 309

private static final long LONGEST_DURATION_NANOS = 10000000000L; // 10秒硬超时
 
static boolean isAnimRunning(...) {
    boolean isRunning = !checker.isAtEquilibrium(easeStyle, value, velocity);
    if (isRunning && totalTimeNanos > LONGEST_DURATION_NANOS) {
        isRunning = false;  // 10 秒超时强制结束
    }
    return isRunning;
}

但在此 trace 中,动画在约 2 秒时结束(可能是因为该动画属性是批量动画组的一部分,其他属性如 translationYminVisibleChange=1f)正常收敛后触发了整组结束)。

3.5 为什么 minVisibleChange = -1 时不会有问题

// FolmeCore.java:110-111
if (data.property.getMinVisibleChange() == -1f) {
    data.property.setMinVisibleChange(target.getMinVisibleChange(data.property.getName()));
}

如果传 -1f(默认值),folme 会通过 IAnimTarget.getMinVisibleChange() 自动推断合适的阈值。但传 0f 跳过了这个逻辑,被视为”有效值”。


四、动画参数

spring_phy(1, 0.18, 1)
参数含义
damping阻尼比1 (临界阻尼)
response响应时间0.18s
bounce弹跳1

临界阻尼 (damping=1) 意味着无 overshoot,值单调趋近目标。这也解释了为什么曲线是纯指数衰减形态。


五、修复方案

minVisibleChange0f 改为 1f(像素级别的最小可见变化):

// AllInOneClockAnimation.kt:455
// Before:
private val MARGAZINE_TRANSLATEY: ValueProperty<Int> = ValueProperty("magazine_translateY", 0f)
 
// After:
private val MARGAZINE_TRANSLATEY: ValueProperty<Int> = ValueProperty("magazine_translateY", 1f)

修复后阈值:

  • mValueThreshold = 1f * 0.75f = 0.75f(距目标 0.75px 以内视为到达)
  • mVelocityThreshold = 0.75f * 16.67 = 12.5f(速度低于 12.5 px/s 视为静止)

预估效果:动画将在 ~300ms 时判定收敛(位移 <0.75px 且速度 <12.5px/s),节省约 700ms + 1100ms 空跑 = 1800ms 的无效帧刷新


六、影响范围

仅影响 all_in_one_clock 动画组中的 magazine_translateY 属性:

  • 指纹解锁时锁屏时钟画报的 Y 轴位移动画
  • 修复后视觉效果无任何差异(0.75px 位移不可见)

七、排查工具链

本次排查使用的工具:

  1. FolmeTracer 打点 — 获取动画 ID、参数、开始/结束时间
  2. Perfetto Trace — 时间线上定位动画 slice,与系统帧对齐
  3. folme_design log — 逐帧 magazine_translateY 值,生成曲线
  4. Python matplotlib — 可视化曲线,明确看到收敛点和空跑段

排查流程:

FolmeTracer 发现动画时长异常 → Perfetto 定位时间段 → 
folme_design log 提取逐帧值 → 绘制曲线发现空跑 → 
分析 EquilibriumChecker 代码 → 定位 minVisibleChange=0f 根因