属性动画
属性动画:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。
学习目标
- 能沿“属性动画”的用户事件解释Android组件、状态所有者、线程与销毁边界。
- 能围绕“掌握 Android 属性动画系统——ValueAnimator、ObjectAnimator、插值器、AnimatorSet 与 ViewPropertyAnimator,让界面动起来。”改出一个可运行结果,并用前后状态而非组件数量验收。
- 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
- 能用输入序列、模型状态、渲染边界、取消和reduced-motion终态独立重放结论,并标明第四版机制与现代targetSdk政策的边界。
让界面动起来——从僵硬的"闪现"到有质感的过渡
早期的 Android 只有"补间动画"。它能让 View 看起来在移动、缩放、旋转——但实际上 View 的真实位置和属性从未改变。你看着按钮飘到了右边,但点左边它才响应——因为它的触摸区域还在原位。这就是"假动画"。
Android 3.0 引入了 属性动画(Property Animation)。它直接修改对象的真实属性——translationX、alpha、scaleX 等——而不是"假装"改了。位置真的变了,点击区域也跟着走。这才是"真正的动画"。
用电影的帧率来类比:属性动画就像在 1 秒内播 60 帧——每一帧都算出当前时间点属性应该是多少,然后直接改在对象上。插值器决定了 60 帧怎么分配(匀速?先快后慢?),估值器决定了中间值怎么算。
① ValueAnimator 驱动数值
ValueAnimator.ofFloat(0f, 1f) 在 duration
毫秒内从起始值平滑过渡到结束值。它不直接修改任何 View 的属性——而是通过
addUpdateListener
回调把每一帧的当前值交给你,你自己决定怎么用(比如更新自定义 View 的
progress 字段并调 invalidate())。
1. ValueAnimator——动画的心脏
<Term def="属性动画的基础——在时长内从起始值过渡到结束值,通过 addUpdateListener 获取每一帧的当前值">ValueAnimator</Term> 不直接作用于任何 View。它只是一个"值生成器"——在 duration 毫秒内,从 0f 平滑变到 1f,每帧回调告诉你当前值是多少。你拿到这个值后自己决定做什么。
// 从 0 到 1 的平滑过渡,持续 1000ms
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 1000
addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
// 你可以用 fraction 做任何事——跟 View 解耦
customView.progress = fraction
customView.invalidate()
}
start()
}三种工厂方法
ValueAnimator.ofFloat(0f, 1f) // float 值动画
ValueAnimator.ofInt(0, 100) // int 值动画
ValueAnimator.ofObject(TypeEvaluator, start, end) // 自定义类型动画(如 PointF)自定义 TypeEvaluator——让动画处理任意类型
当你想让 PointF、Color 甚至自定义数据类型参与动画:
// 让 PointF 支持动画
ValueAnimator.ofObject(
object : TypeEvaluator<PointF> {
override fun evaluate(fraction: Float, startValue: PointF, endValue: PointF): PointF {
val x = startValue.x + (endValue.x - startValue.x) * fraction
val y = startValue.y + (endValue.y - startValue.y) * fraction
return PointF(x, y)
}
},
PointF(0f, 0f), // 起点
PointF(100f, 200f) // 终点
).apply {
duration = 1500
addUpdateListener { animation ->
val point = animation.animatedValue as PointF
customView.moveTo(point.x, point.y)
}
start()
}2. ObjectAnimator——直接作用于对象的属性
<Term def="ValueAnimator 的子类——通过反射直接修改目标对象指定属性的值,如 translationX、alpha、backgroundColor">ObjectAnimator</Term> 是 ValueAnimator 的子类。它更进一步:直接通过反射调用目标对象的 setter 方法,自动更新属性值。
// alpha 从 1.0 变到 0.0(淡出)
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).apply {
duration = 500
start()
}
// 从当前位置向右移动 200px
ObjectAnimator.ofFloat(view, "translationX", 0f, 200f).apply {
duration = 800
start()
}
// 组合的 ObjectAnimator——同时变透明+缩小
val fadeOut = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f)
val scaleDown = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.5f)
scaleDown.setFloatValues(1f, 0.5f) // scaleY 同理
AnimatorSet().apply {
playTogether(fadeOut, scaleDown)
duration = 400
start()
}3. AnimatorSet——编排多动画
单个动画往往不够——你需要"先淡入,再放大,最后弹一下"这种多步骤编排。<Term def="将多个 Animator 组合在一起,支持同时播放(playTogether)和顺序播放(playSequentially)">AnimatorSet</Term> 就是做这个的:
val fadeIn = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f)
val scaleUpX = ObjectAnimator.ofFloat(view, "scaleX", 0.5f, 1f)
val scaleUpY = ObjectAnimator.ofFloat(view, "scaleY", 0.5f, 1f)
val bounce = ObjectAnimator.ofFloat(view, "translationY", 0f, -30f, 0f)
AnimatorSet().apply {
// 第一阶段:淡入+放大同时进行
play(fadeIn).with(scaleUpX)
play(scaleUpX).with(scaleUpY)
// 第二阶段:弹一下
play(bounce).after(fadeIn)
duration = 600
start()
}play(a).with(b) —— a 和 b 同时播放;play(b).after(a) —— b 在 a 结束后播放。可以链式组合出任意编排。
4. Interpolator——让动画有"性格"
<Term def="控制动画随时间推进的速率曲线——匀速、加速、减速、弹跳、回弹等">Interpolator</Term> 决定了动画的"节奏感"。同一段 500ms 的位移动画,不同插值器效果完全不同:
ObjectAnimator.ofFloat(view, "translationX", 0f, 300f).apply {
duration = 1000
interpolator = AccelerateDecelerateInterpolator() // 先加速后减速(最自然)
start()
}第 1 / 5 步 · ① ObjectAnimator.ofFloat(view,"translationX",0f,300f):目标方块停在起点,translationX = 0
点击播放:左侧方块沿 translationX 平移、右侧进度点沿插值曲线移动、底部读数同步跳动——三者同一时间窗严格对应。可暂停、单步、拖进度逐帧观察 S 形的慢→快→慢。
常用插值器一览:
| Interpolator | 效果 | 适用场景 |
|---|---|---|
LinearInterpolator | 匀速 | 进度条、机械运动 |
AccelerateInterpolator | 慢 → 快 | 元素"飞出去" |
DecelerateInterpolator | 快 → 慢 | 元素"停稳" |
AccelerateDecelerateInterpolator | 慢 → 快 → 慢 | 最自然的过渡(默认) |
OvershootInterpolator | 冲过头再回来 | "弹性"效果,俏皮感 |
BounceInterpolator | 末尾弹跳 | 落下弹起的效果 |
AnticipateInterpolator | 先反向再正向 | 蓄力发射 |
AnticipateOvershootInterpolator | 蓄力→冲过头→回来 | 夸张的弹性效果 |
5. 动画生命周期监听
Animator.AnimatorListener 提供完整的生命周期回调:
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).apply {
duration = 500
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
view.isEnabled = false // 动画期间禁用点击
}
override fun onAnimationEnd(animation: Animator) {
view.visibility = View.GONE // 动画结束后隐藏
}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
start()
}如果不需要所有回调,可以用 AnimatorListenerAdapter 只覆写需要的方法:
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
view.visibility = View.GONE
}
})6. ViewPropertyAnimator——View 动画的快捷方式
如果只是做 View 的属性动画,不需要编排,不需要自定义估值器——ViewPropertyAnimator 是最简洁的方式:
// 链式调用:一行写完"向右移 200px + 半透明 + 旋转 360°"
view.animate()
.translationX(200f)
.alpha(0.5f)
.rotation(360f)
.setDuration(800)
.setInterpolator(AccelerateDecelerateInterpolator())
.withEndAction {
// 动画结束后的操作
view.visibility = View.GONE
}
.start()view.animate() 返回 ViewPropertyAnimator,支持 translationX/Y/Z、alpha、scaleX/Y、rotation/X/Y、x/y。比 ObjectAnimator 更简洁,但功能更受限——不能编排多个 View,不能自定义估值器。
7. 属性动画 vs 补间动画——本质区别
| 方面 | 补间动画(View Animation) | 属性动画(Property Animation) |
|---|---|---|
| 修改什么 | 只修改显示位置(Matrix 变换) | 修改对象的真实属性 |
| 点击区域 | 不跟随动画移动 | 跟随真实位置 |
| 作用对象 | 只能是 View | 任意对象(包括非 View) |
| API 风格 | XML 定义为主 | 代码构建为主(也支持 XML) |
| 引入版本 | API 1 | API 11(Android 3.0) |
一句话总结:补间动画是"视觉上动了",属性动画是"真的动了"。新项目应该全面使用属性动画。
容易踩的坑
小结
- 属性动画直接修改对象真实属性——位置、透明度、缩放等真的变了;补间动画只改视觉显示
ValueAnimator生成值,你在 UpdateListener 中决定怎么用;ObjectAnimator通过反射自动改属性AnimatorSet编排多动画顺序:playTogether同时播放、playSequentially顺序播放- 插值器决定节奏:AccelerateDecelerate(最自然)、Overshoot(弹性)、Bounce(弹跳)、Linear(匀速)
view.animate()是 View 动画的最简方式——链式调用、自动跟随生命周期- Activity 退出时记得 cancel 动画,防止内存泄漏
练习
问题 1:“第31章 Property Animation”覆盖哪些正式节点和项目主线?
问题 2:怎样建立本页最小可执行实验?
问题 3:为什么只在正常点击路径运行不能证明完成?
问题 4:怎样设计能推翻当前实现的反例?
问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?
问题 6:本页达到独立交接标准需要什么?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- ValueAnimator
属性动画的基类——在 duration 毫秒内从起始值平滑过渡到结束值。不直接作用于任何对象,需要通过 addUpdateListener 获取每帧的当前值,手动决定如何使用。
- ObjectAnimator
ValueAnimator 的子类——通过反射自动调用目标对象的 setter 方法修改属性。如
ofFloat(view, "alpha", 1f, 0f)会调用view.setAlpha()。要求目标属性有对应的 setter 方法。- AnimatorSet
多个动画的编排器。支持
playTogether(同时播放)和playSequentially(顺序播放),以及play(a).with(b).after(c)的组合编排语法。- Interpolator
动画的节奏控制器。决定动画值随时间变化的速率曲线。Linear(匀速)、AccelerateDecelerate(慢→快→慢)、Overshoot(冲过头再回来)、Bounce(末尾弹跳)。
- 补间动画 (View Animation)
Android 早期的动画系统,只修改 View 的绘制矩阵(视觉效果),不改变 View 的真实位置和属性。点击区域不会跟随动画移动。API 1 引入,不推荐在新项目中使用。
“属性动画”不使用未获授权的纸书正文;InformIT出版信息与授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。
为什么“属性动画”必须回到可观察状态
“属性动画”的学习结果不是记住类名,而是能预测“掌握 Android 属性动画系统——ValueAnimator、ObjectAnimator、插值器、AnimatorSet 与 ViewPropertyAnimator,让界面动起来。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。
第四版机制逐项深读
31. Property Animation
在“属性动画”中,“31. Property Animation”的多个Animator需显式定义依赖与取消传播;同时播放不代表生命周期会自动收束。
Building the Scene
在“属性动画”中,分析“Building the Scene”先冻结设备API、targetSdk和输入,再沿回调与数据流寻找首个状态分叉,不能只描述最终页面。
Simple Property Animation
在“属性动画”中,“Simple Property Animation”的多个Animator需显式定义依赖与取消传播;同时播放不代表生命周期会自动收束。
Playing Animators Together
在“属性动画”中,“Playing Animators Together”随时间改变对象属性并触发重绘;起点、终点、插值器与取消终态共同构成可验证合同。
For the More Curious: Other Animation APIs
在“属性动画”中,验证“For the More Curious: Other Animation APIs”在重复启动、反向、离开页面和reduced motion下比较终态,不能依赖最后一帧偶然落点。
Challenges
在“属性动画”中,“Challenges”用于推翻“掌握 Android 属性动画系统——ValueAnimator、ObjectAnimator、插值器、AnimatorSet 与 ViewPropertyAnimator,让界面动起来。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。
“属性动画”验收回顾
“属性动画”只有在输入序列、模型状态、渲染边界、取消和reduced-motion终态能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。
章专属可重放状态实验
先预测“启动、暂停、取消、组合动画、旋转与减少动态效果”发生后,Animator、View 属性与场景状态机应怎样改变属性起止值、时长、插值、运行阶段和点击区域;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。
实验一:所有者—状态—结果合同
选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。
Owner · state · observable result
属性动画:状态合同
用 ObjectAnimator 与 AnimatorSet 改变真实属性并同步交互边界
验证场景
第四版正式目录节点
property-animation · 正常任务
31. Property Animation:固定 SDK、设备配置和初始状态,触发“启动、暂停、取消、组合动画、旋转与减少动态效果”
冻结入口:31. Property Animation
记录Animator、View 属性与场景状态机的初始属性起止值、时长、插值、运行阶段和点击区域
观察:属性快照、animator 状态、点击坐标、取消轨迹和可访问性边界中的“31. Property Animation”轨迹
预期:由Animator、View 属性与场景状态机提交属性起止值、时长、插值、运行阶段和点击区域,并持续满足“动画结束后的视觉、布局点击区域和无障碍边界一致”
实验二:事件与生命周期轨迹
沿五次转换逐步执行“启动、暂停、取消、组合动画、旋转与减少动态效果”。每一步只允许Animator、View 属性与场景状态机按职责提交状态,并持续核对“动画结束后的视觉、布局点击区域和无障碍边界一致”。
Deterministic event replay
属性动画:事件轨迹
不变量:动画结束后的视觉、布局点击区域和无障碍边界一致
交付证据:属性快照、animator 状态、点击坐标、取消轨迹和可访问性边界
实验三:章专属反例与同输入恢复
注入“只做 canvas 位移,按钮看似移动但点击区域仍留在原位”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有属性快照、animator 状态、点击坐标、取消轨迹和可访问性边界一起恢复才算修复。
Fault · cancel · restore
属性动画:反例与恢复
故障:只做 canvas 位移,按钮看似移动但点击区域仍留在原位
第 1 次使用相同 SDK、设备配置、初始状态与用户事件
保持正常输入不变,仅注入“只做 canvas 位移,按钮看似移动但点击区域仍留在原位”
动画结束后的视觉、布局点击区域和无障碍边界一致
属性快照、animator 状态、点击坐标、取消轨迹和可访问性边界