要点
Timeline — 是动画的时间模型,决定了动画何时开始、持续多长时间以及时间的流逝如何转化为被动画参数的变化。与逐帧动画(frame-by-frame)不同,逐帧动画中每一帧都是手动设置的,而时间线动画则通过参数描述:duration — 总持续时间,startDelay — 开始前的延迟,repeatCount — 重复次数,以及 timingFunction — 速度曲线。
时间线在标准化空间 [0, 1] 中工作 — local time 或 progress。零值对应动画开始,一对应完成。缓动函数将线性 local time 转换为实际动画进度。如果 repeatCount > 1,时间线会循环重置(RESTART)或反转(REVERSE)。在 iOS 中,CAMediaTiming 添加了 speed(播放速度,>1 加速)和 timeOffset(时间线偏移)参数。在 Android 中,Animator 使用 setCurrentPlayTime() 直接控制时间线上的位置。根据 Material Design,短于 100 毫秒的动画不会被感知为动画,而长于 500 毫秒的动画则开始令人烦躁。界面动画的最佳持续时间是 200–350 毫秒。
在 iOS 中,动画时间线的管理通过 CAMediaTiming 协议进行,该协议由 CAAnimation 和 CALayer 实现。它提供以下属性:duration(以秒为单位的持续时间)、beginTime(相对于父级时间线的开始时间)、repeatCount(重复次数)、repeatDuration(重复的总持续时间)、autoreverses(自动反向)、fillMode(动画之前/之后的行为)、speed(播放速度)和 timeOffset(时间线上的偏移)。
CACurrentMediaTime() — 返回自设备上次重启以来的绝对时间(以秒为单位)的函数。用作多个动画同步的基本时间戳:设置 beginTime = CACurrentMediaTime() + offset 以精确延迟启动一组动画。CAMediaTiming 还支持时间线层次结构——父图层可以通过 speed 属性加速/减速所有子动画。在 UIKit 中,UIViewPropertyAnimator(iOS 10+)通过 duration、delay 属性和 startAnimation/pauseAnimation/stopAnimation 回调来抽象时间线。在 SwiftUI 中,Animation 结构提供 duration 作为动画规范的一部分,但时间线的直接控制是隐藏的——对于自定义时间线,请使用带有自己调度器的 TimelineView(iOS 15+)。
在 Android 中,时间线的管理集中在 Animator 类及其子类(ValueAnimator、ObjectAnimator)中。时间线的主要属性:duration(以毫秒为单位的持续时间)、startDelay(开始前的延迟)、repeatCount(重复次数)、repeatMode(RESTART 或 REVERSE)。setCurrentPlayTime(long) 方法允许沿时间线移动到任意位置。
ValueAnimator 增加了一个抽象层:基于 duration 生成从 0 到 1 的值(normalized time),并将它们传递给 TimeInterpolator。AnimatorSet 允许将多个 Animator 组合到单一时间线中,支持顺序(playSequentially)或并行(playTogether)执行。在 Jetpack Compose 中,时间线通过 AnimationSpec 管理:tween(durationMillis、delayMillis、easing)、spring(dampingRatio、stiffness)和 keyframes(时间线上的点,每个段都有自定义缓动)。Compose AnimatedContent 使用时间线进行 transitionSpec — 开发者描述内容如何随时间偏移出现/消失。根据 Android Performance,动画时间线应为 16 毫秒(vsync 周期)的倍数以保持 60fps — 错误的持续时间可能因与屏幕刷新率不匹配而导致丢帧。
时间线在组合多个动画时变得尤为重要。三种基本模式:级联 — 动画按顺序带偏移启动(每个后续在前一个偏移之后开始),并行 — 所有动画同时以相同时间线启动,交错 — 动画重叠启动(第二个在第一个完成之前开始)。
在 iOS 中,级联通过每个 CAAnimation 的 beginTime = 上一个 beginTime + 上一个 duration 实现。对于交错,使用 CAAnimationGroup 在嵌套动画中设置不同的 beginTime。在 Android 中,AnimatorSet 支持 playSequentially 和 playTogether;通过为每个 Animator 设置 startDelay 来创建交错效果。在 Compose 中,AnimatedVisibility 配合 enter/exitTransition 使用 staggerChildren 变体中的交错来实现列表元素的级联出现。在 WWDC 和 Google I/O 2024 上,两个平台都推出了时间线可视化工具:Xcode Animations Inspector 和 Android Studio Animations Timeline — 它们实时显示实际时间线和丢失的帧。
自定义时间线动画示例:延迟 0.3 秒,持续时间 0.6 秒,自动反向和无限重复。CAMediaTiming 管理所有时间参数。
import UIKit
func breathingAnimation(layer: CALayer) {
let animation = CABasicAnimation(keyPath: "opacity")
animation.duration = 0.6
animation.beginTime = CACurrentMediaTime() + 0.3
animation.fromValue = 1.0
animation.toValue = 0.3
animation.autoreverses = true
animation.repeatCount = .infinity
animation.speed = 1.0
animation.fillMode = .both
layer.add(animation, forKey: "breathing")
}
beginTime = CACurrentMediaTime() + 0.3 提供 300 毫秒的延迟。autoreverses = true 使动画在每个周期后反向运行。repeatCount = .infinity 创建无限的"呼吸"效果循环。speed = 1.0 — 正常速度;用值 2.0 将动画加速 2 倍。
SwiftUI TimelineView(iOS 15+)提供对系统时间线的访问以进行自定义动画。视图在每次调度器计时时更新。
import SwiftUI
struct TimelineViewExample: View {
var body: some View {
TimelineView(.periodic(from: .now, by: 0.1)) { context in
let seconds = context.date.timeIntervalSinceReferenceDate
let phase = sin(seconds * 2 * .pi)
Circle()
.fill(Color.blue)
.frame(width: 60 + 30 * phase)
.animation(.linear(duration: 0), value: phase)
}
}
}
TimelineView 配合 .periodic 调度器每 0.1 秒生成一次计时。在闭包中,context.date — 当前时间,被转换为正弦波相位。圆的大小以 1 Hz 的频率脉动。TimelineView 是在没有 Animation API 的情况下在 SwiftUI 中创建具有自己时间线的动画的唯一方法。
ValueAnimator 完全控制时间线:延迟、持续时间、重复。setCurrentPlayTime 方法允许沿时间线跳跃。
import android.animation.ValueAnimator
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.View
fun View.timelineAnimation() {
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 400
startDelay = 200
repeatCount = 2
repeatMode = ValueAnimator.REVERSE
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animator ->
val progress = animator.animatedFraction
alpha = progress
translationX = progress * 100f
}
start()
}
}
startDelay = 200 毫秒 — 开始前的延迟。repeatMode = REVERSE 将动画返回到初始值。animatedFraction 提供从 0 到 1 的进度,同时考虑 repeatMode。addUpdateListener 每帧触发(通常为 16 毫秒)。要暂停,请使用 pause() / resume()。
AnimatorSet 允许从多个动画创建复杂的时间线。在示例中 — 带重叠的顺序执行(交错)。
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
fun View.staggerAnimation() {
val fadeIn = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f).apply {
duration = 300
}
val slideUp = ObjectAnimator.ofFloat(this, "translationY", 100f, 0f).apply {
duration = 400
startDelay = 100
}
val scale = ObjectAnimator.ofFloat(this, "scaleX", 0.8f, 1f).apply {
duration = 250
startDelay = 200
}
AnimatorSet().apply {
playTogether(fadeIn, slideUp, scale)
start()
}
}
playTogether 并行启动所有动画,但每个都有自己的 startDelay — 这创建了交错效果:fadeIn 首先启动(0 毫秒),slideUp 在 100 毫秒后,scale 在 200 毫秒后。最终时间线:0 — 100 — 200 — 600 毫秒。对于顺序执行,请使用 playSequentially。
常见问题
Animator — 所有动画的抽象基类。ValueAnimator 是其子类,用于动画数值而不绑定到对象。ObjectAnimator(ValueAnimator 的后代)动画对象的特定属性(例如 alpha 或 translationY)。对于时间线,请使用 ValueAnimator 作为通用计时器,通过 addUpdateListener 进行回调。
使用系统时钟(iOS 中的 CACurrentMediaTime,Android 中的 System.nanoTime)计算进度,而不是帧数。对于网络同步,使用带偏移校正的基于 NTP 的计时器。iOS 支持 AVAudioSession 进行毫秒级精度的音视频时间线同步。避免基于帧的同步 — 帧率从 30 到 120 fps 不等。
startOffset — 动画开始前的延迟(以毫秒为单位)。用于级联和交错效果。repeatCount — 重复次数:0 = 不重复,ValueAnimator.INFINITE = 无限循环。repeatMode 确定方向:RESTART(重置到开始)或 REVERSE(反向运动)。在 iOS 中,startOffset 称为 beginTime(与 CACurrentMediaTime 结合使用),repeatCount — CABasicAnimation 的同名属性。
Material Design 建议界面动画使用 200–350 毫秒。短于 100 毫秒的动画不会被感知为动画,长于 500 毫秒的动画会令人烦躁。对于元素的进入/退出:200–300 毫秒。对于屏幕之间的过渡:300–400 毫秒。对小元素使用较短的持续时间(100–200 毫秒),对大元素使用较长的持续时间(400–500 毫秒)。
卡顿(jank)由以下原因引起:主线程中的长时间操作(阻塞渲染)、带有过度绘制的复杂视图/图层层次结构、或时间线的不正确使用(持续时间不是 vsync 的倍数 — 60fps 为 16 毫秒,120fps 为 8 毫秒)。使用 Xcode Instruments Animations 或 Android Studio Profiler 检测丢失的帧。在 Compose 中,使用 Modifier.drawWithCache 缓存复杂绘制。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。