属性动画

属性动画:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。

学习目标

  • 能沿“属性动画”的用户事件解释Android组件、状态所有者、线程与销毁边界。
  • 能围绕“掌握 Android 属性动画系统——ValueAnimator、ObjectAnimator、插值器、AnimatorSet 与 ViewPropertyAnimator,让界面动起来。”改出一个可运行结果,并用前后状态而非组件数量验收。
  • 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
  • 能用输入序列、模型状态、渲染边界、取消和reduced-motion终态独立重放结论,并标明第四版机制与现代targetSdk政策的边界。

让界面动起来——从僵硬的"闪现"到有质感的过渡

早期的 Android 只有"补间动画"。它能让 View 看起来在移动、缩放、旋转——但实际上 View 的真实位置和属性从未改变。你看着按钮飘到了右边,但点左边它才响应——因为它的触摸区域还在原位。这就是"假动画"。

Android 3.0 引入了 属性动画(Property Animation)。它直接修改对象的真实属性——translationXalphascaleX 等——而不是"假装"改了。位置真的变了,点击区域也跟着走。这才是"真正的动画"。

电影的帧率来类比:属性动画就像在 1 秒内播 60 帧——每一帧都算出当前时间点属性应该是多少,然后直接改在对象上。插值器决定了 60 帧怎么分配(匀速?先快后慢?),估值器决定了中间值怎么算。

分步1 / 4

① 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()
}
可交互
目标对象(ObjectAnimator 反射 set 属性)0300view当前属性值(ValueAnimator 只算值,需手动 set):translationX = 0TimeInterpolator(控制快慢节奏)10fraction时间进度 →匀速(Linear)参照AccelerateDecelerate(慢→快→慢)多个这样的动画可用 AnimatorSet 编排(同时 / 顺序)

第 1 / 5 步 · ① ObjectAnimator.ofFloat(view,"translationX",0f,300f):目标方块停在起点,translationX = 0

点击播放:左侧方块沿 translationX 平移、右侧进度点沿插值曲线移动、底部读数同步跳动——三者同一时间窗严格对应。可暂停、单步、拖进度逐帧观察 S 形的慢→快→慢。

属性动画原理:ValueAnimator 每帧按 TimeInterpolator(这里是 S 形的 AccelerateDecelerate)算出 0~1 的 fraction,再映射成 translationX; ObjectAnimator 则通过反射自动把这个值 set 到 view 上——所以方块的位移 与曲线高度一一对应:慢启动、中段加速、临近终点减速。

常用插值器一览:

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/ZalphascaleX/Yrotation/X/Yx/y。比 ObjectAnimator 更简洁,但功能更受限——不能编排多个 View,不能自定义估值器。

7. 属性动画 vs 补间动画——本质区别

方面补间动画(View Animation)属性动画(Property Animation)
修改什么只修改显示位置(Matrix 变换)修改对象的真实属性
点击区域不跟随动画移动跟随真实位置
作用对象只能是 View任意对象(包括非 View)
API 风格XML 定义为主代码构建为主(也支持 XML)
引入版本API 1API 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、设备配置和初始状态,触发“启动、暂停、取消、组合动画、旋转与减少动态效果”

状态所有者Animator、View 属性与场景状态机
受控状态属性起止值、时长、插值、运行阶段和点击区域
触发事件启动、暂停、取消、组合动画、旋转与减少动态效果

冻结入口:31. Property Animation

记录Animator、View 属性与场景状态机的初始属性起止值、时长、插值、运行阶段和点击区域

观察:属性快照、animator 状态、点击坐标、取消轨迹和可访问性边界中的“31. Property Animation”轨迹

预期:由Animator、View 属性与场景状态机提交属性起止值、时长、插值、运行阶段和点击区域,并持续满足“动画结束后的视觉、布局点击区域和无障碍边界一致”

实验二:事件与生命周期轨迹

沿五次转换逐步执行“启动、暂停、取消、组合动画、旋转与减少动态效果”。每一步只允许Animator、View 属性与场景状态机按职责提交状态,并持续核对“动画结束后的视觉、布局点击区域和无障碍边界一致”。

Deterministic event replay

属性动画:事件轨迹

选择一次状态转换1 / 5

不变量:动画结束后的视觉、布局点击区域和无障碍边界一致

交付证据:属性快照、animator 状态、点击坐标、取消轨迹和可访问性边界

实验三:章专属反例与同输入恢复

注入“只做 canvas 位移,按钮看似移动但点击区域仍留在原位”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有属性快照、animator 状态、点击坐标、取消轨迹和可访问性边界一起恢复才算修复。

Fault · cancel · restore

属性动画:反例与恢复

故障:只做 canvas 位移,按钮看似移动但点击区域仍留在原位

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“只做 canvas 位移,按钮看似移动但点击区域仍留在原位”

3. 检查所有者一致

动画结束后的视觉、布局点击区域和无障碍边界一致

4. 核对结果一致

属性快照、animator 状态、点击坐标、取消轨迹和可访问性边界

讨论

评论区加载中…