Skip to content

动画:属性动画系统、物理动效与 vsync 帧节奏 ​

本页导读:动画的本质是「按帧把时间映射成属性」。本页拆开属性动画的帧循环(AnimationHandler ↔ Choreographer)、插值器与估值器的分工、硬件层为何让平移缩放近乎免费、SpringAnimation 物理动效的参数语义,最后落到「卡顿怎么定位」。前置:View 体系、事件分发。

一、动画的三代形态:帧动画、补间、属性 ​

  • 帧动画(AnimationDrawable 逐帧切图):内存与包体灾难,AndroidX 时代仅剩 GIF 播放器的历史意义。
  • 补间动画 ViewAnimation(android.view.animation):只改绘制矩阵不改真实属性——点了「动画位置」上的 View 没反应的经典根源,已进遗产名单。
  • 属性动画(android.animation,API 11+):真改属性(setter/反射式 Property),可监听、可打断、可插值任意值——当代唯一推荐。
kotlin
// 属性动画最小核:ValueAnimator 只管产数,不管画什么
val anim = ValueAnimator.ofFloat(0f, 1f).apply {
    duration = 300
    interpolator = PathInterpolator(0.4f, 0f, 0.2f, 1f)  // Material 标准曲线
    addUpdateListener { view.alpha = it.animatedValue as Float }
}
anim.start()

- 生命周期绑定三件套:`start/cancel/end`;Activity 退场要 cancel,否则监听器攥着 View 不放就是泄漏;
- `doOnPreDraw` + 首帧后启动入场动画,避开 inflate 与动画抢同帧。

二、帧循环:AnimationHandler 与 doAnimationFrame ​

属性动画不是自己跑线程,而是挂进与绘制同一个 vsync 节拍:

java
// frameworks/base/core/java/android/view/animation/AnimationHandler.java(机制大意)
// ValueAnimator 注册进 AnimationHandler → 它向 Choreographer 注册 CALLBACK_ANIMATION;
// doFrame 到达 → doAnimationFrame(frameTimeNanos):
//   1. 计算 elapsed/duration → fraction
//   2. interpolator.getInterpolation(fraction) → interpolated
//   3. evaluator.evaluate(interpolated, start, end) → 值
//   4. 回调 updateListener / 写目标属性 → 触发 invalidate
// 所有仍在跑的动画共享同一次 vsync 回调 —— 一次唤醒,全体推进

推论三条:

  • 同进程所有 ValueAnimator 只注册一次 vsync 回调(AnimationHandler 聚合),这是「动画数量不影响唤醒成本、update 回调内容才影响」的依据。
  1. 动画「掉帧」与主线程阻塞是同一件事——回调在主线程;真正重绘在 RenderThread,但产数在主线程。
  2. Choreographer.CALLBACK_ANIMATION 先于 CALLBACK_TRAVERSAL 执行:动画值写完,本帧遍历直接带上新状态。
  3. 后台动画会被节流:进程 cached/冻结时 vsync 停发,动画「暂停」而非继续(freezer 见进程模型)。

三、插值器与估值器:一个是节奏,一个是类型 ​

工具职责常用代表
TimeInterpolator把线性时间 fraction 映射成节奏AccelerateDecelerateInterpolator(ValueAnimator 默认)、FastOutSlowIn(PathInterpolator(0.4,0,0.2,1))、Anticipate/Overshoot、线性
TypeEvaluator把 0~1 映射成目标类型的一个值Int/Float/Argb/时间;自定义:点集、贝塞尔轨迹
Keyframes时间轴多锚点Keyframes.of(0f,a,0.6f,b,1f,c) + PropertyValuesHolder

选择口径:

  • 入场用 decelerate 系(从快到慢「滑进来」),退场用 accelerate 系(「被吸走」)——物理直觉即用户直觉;
  • 弹性回弹(Overshoot)只在强调「可交互」时用,列表滚动禁用回弹是官方 Material 明确口径;
  • 颜色动画一律 ArgbEvaluator(或与色彩空间一致的自研 evaluator),直接 int 线性插值会「灰脏」。
kotlin
// Keyframes 多段:先冲过头再回落(强调反馈)
val kf = Keyframes.of(0f, 0f, 0.45f, 1.08f, 1f, 1f)
val pa = PropertyValuesHolder.ofFloat("progress", kf)
ObjectAnimator.ofPropertyValuesHolder(view, pa).apply { duration = 320 }.start()
  • AnimatorSet 的 playWith 编排(before/after/with)仍是复杂转场最短路径;共享元素转场走 ActivityOptions 的 SceneTransition 家族,别手搓位移动画假装转场。

四、SpringAnimation:从「时长」到「物理」 ​

AndroidX androidx.dynamicanimation(SpringAnimation/SpringForce/DynamicAnimation)把动画参数从「多少毫秒」换成物理量:

  • stiffness(弹簧劲度,LOW/MEDIUM/HIGH 预设 + 自定义):越大越快越「硬」;

  • dampingRatio(阻尼比):1.0 临界不震荡;<1 带振荡(Q 弹感);典型 UI 在 0.6~1.0;

  • 没有 duration:结束由系统判定「离目标足够近且速度足够低」——动画时长变成物理结果。

  • 判定「弹簧还是时长动画」:会被手势打断吗?会 → 弹簧;纯展示节奏固定 → 时长曲线足够。

  • 天然可中断:手势打断直接改速度重接弹簧(setStartVelocity),这就是拖拽跟手动画(bottom sheet、可拖卡片)的标准实现;FlingAnimator 则承接惯性(初速度 + 衰减)。

  • 系统侧同一哲学:MotionSpec/SpringTemplate(Material 组件内部)给出的 stiffness/阻尼预设就是官方「动效方言」,自研组件对齐它们比自定义时长更统一。

kotlin
SpringAnimation(view, View.TRANSLATION_Y).apply {
    spring = SpringForce(0f).apply {
        dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM   // 中阻尼,轻微回弹
        stiffness = SpringForce.STIFFNESS_LOW
    }
    flingVelocity = -velocityTracker.velocity   // 手势速度直接注入
    start()
}

版本注记:基于 Android 16 / API 36;API 11 属性动画登场;API 31 公开 RenderNode/RenderEffect 给动画开硬件玩法;Android 12+ 系统动效全面转向物理曲线(Material You 的 spring 语言),旧 DecelerateInterpolator(1.5f) 风格的自研曲线应逐步对齐 MotionSpec/SpringTemplate 体系。

五、帧节奏:Choreographer 与 vsync 的真相 ​

  • vsync 由显示系统产生,经 SF → DisplayEventReceiver → Choreographer(每进程一个,主线程 Looper 上)统一发令;postFrameCallback 即订阅下一帧。

  • 一帧回调顺序:CALLBACK_INPUT → CALLBACK_ANIMATION → CALLBACK_TRAVERSAL(即 ViewRootImpl 遍历)→ CALLBACK_COMMIT;你的动画代码与遍历同呼吸,动画天然与绘制同步。

  • 掉帧(jank)的本质:doFrame 里所有主线程活儿加起来超过帧预算(60Hz 16.7ms、120Hz 8.3ms)——SF 只能继续贴上一帧。

  • 「动画驱动」的自研动效别自己 while + sleep,用 postFrameCallback/ValueAnimator 借同一节拍,否则动画和 vsync 打架必然抖。

  • 高刷屏上 Choreographer 报的是实际刷新周期的 vsync;「16.7ms 预算」只是 60Hz 假设,帧率自适应设备上预算是变量。

  • Davey:一帧迟到的 callback——系统打 log 且 perfetto 里可见 Davey! 切片,是「动画明显一滞」的直接证据。

kotlin
// 自研每帧逻辑的正确挂点
Choreographer.getInstance().postFrameCallback { frameTimeNanos ->
    stepSimulation(frameTimeNanos)          // 模拟推进,别做重 IO
    Choreographer.getInstance().postFrameCallback(this)  // 续订
}

六、硬件层动画:什么免费、什么收费 ​

  • 与输入联动记住回调顺序:input → animation → traversal——手势速度在 input 回调更新、动画在 animation 回调消费,天然不打架。

  • 免费档(渲染属性):translationX/Y、scaleX/Y、rotation、alpha——改 RenderNode 变换,主线程连 DisplayList 都不用重录(见 View 页第六节)。

  • 半免费:width/height/margin/padding 等布局属性——触发 requestLayout 全树代价,能用 scaleX/scaleY 视觉等价替代就别动布局;left/top 老 API 同理。

  • 特效档:RenderEffect(模糊/位移矩阵,API 31+)、ViewPropertyAnimator 链式属性(仍是渲染属性组合)——GPU 执行但占带宽,模糊大面积要测低端机。

  • 组合技巧:进入动画用「硬件属性 + alpha」替代「布局尺寸动画」;列表项高度动画用 RecyclerView 的 ItemAnimator 思路(平移替代重排)。

  • 阴影/裁剪动画(outline + elevation)会强制离屏合成,低端机慎叠大 View;

  • outline/elevation 动画的离屏成本 ≈ 区域面积的函数:把大容器动画拆成子层或降 elevation 再动。

  • ViewPropertyAnimator 链式写法把多属性合并为一次硬件动画——链式是性能写法,不只是语法糖。

七、卡顿分析:从现象到归因 ​

  1. 定性:用户说「卡」——先用开发者选项 GPU 呈现模式/帧率表看是「红条偶发」还是「持续掉帧」。
  2. 定量:Perfetto 抓 FrameTimeline(API 31+ 默认数据源),看三类归因:appDeadlineMissed(主线程超预算)、sfDeadlineMissed(合成侧)、display 相关;配合主线程切片直接指到方法名。
  3. 常见元凶清单:主线程 IO/序列化、onBind 大对象 inflate、动画期间触发 requestLayout、GC 可见帧(分配风暴)、过度绘制、自绘 onDraw 分配。
  4. 动画专项:Profile GPU Rendering 里「动画阶段」长条 = update 回调里干了活;把重计算挪出帧回调或预计算成查表。
  • 帧率自适应:90/120Hz 设备帧预算 11.1/8.3ms——「60Hz 勉强及格」的页面在高刷屏上现原形,性能测试要带高刷档。
  1. 回归防护:Macrobenchmark/JankStatsManager(API 31+)把 jank 率纳入 CI 阈值。
bash
  # 线上轻量采集:帧 metrics(API 31+)
adb shell dumpsys gfxinfo com.example framestats | head -30
  # perfetto 抓帧时间线(数据源与配置格式以 Perfetto 文档为准)
adb shell perfetto -o /data/misc/perfetto_trace.pb -c - < frame.cfg

八、动效时长与缓动的工程口径 ​

  • 微交互(按压/切换)80~150ms;转场 250~350ms;超过 500ms 用户开始「等动画」——宁可砍时长也别砍中断能力;
  • 大面积移动快起步慢落位(fast-out/slow-in);小元素短程弹簧;淡入淡出与位移重叠发生,别排队。

九、常见误区问答 ​

Q1:duration=0 的动画还有意义吗? 有——属性直跳但动画管线(监听、插值、完成事件)仍执行;也用来「对齐一帧」:0 时长 + 帧回调做精确时序点。

Q2:alpha=0 的 View 还吃绘制成本吗? 现代硬件路径下 alpha=0 且无阴影等副作用时可整棵跳过绘制,但参与布局;GONE 才连布局都省。动画前后别留 alpha=0 的常驻复杂子树。

Q3:为什么模拟器里动画掉帧真机正常? GPU 虚拟化 + 宿主调度噪声;动画性能结论只在真机(且覆盖中低端档位)下取。

Q4:ObjectAnimator 和 ViewPropertyAnimator(animate())选哪个?

Q5:动画期间 getWidth() 变了? 渲染属性动画不改布局尺寸——translation/scale 动的是矩阵;需要几何坐标做命中/碰撞,取 getLeft()/getTop() 并叠加动画中的 translation/scale 手动换算。 简单 View 属性组合用 animate()(内部自动合并为单个硬件加速动画);需要精细时间轴/共享/嵌套再上 AnimatorSet/ObjectAnimator。

Q6:动画做完 View 状态回跳? 属性动画结束后的目标值即最终值;若同时存在布局双写(width 动画 + requestLayout)就会打架——一个属性只留一个真相源。

本页小结:属性动画是「vsync 驱动的产数管线」:Choreographer 统一发令、AnimationHandler 集中推进、插值器管节奏、估值器管类型;渲染属性动画近乎免费、布局属性动画步步收费;物理弹簧(SpringAnimation)用刚度和阻尼替代时长,换来可中断可注入速度的跟手感;卡顿归因就一句话——主线程在帧预算内没干完活,用 FrameTimeline 定位到函数级。

参考链接 ​

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处