文章

Android 进阶 - 动画与硬件离屏缓冲

ViewPropertyAnimator

属性动画是 Android 中最基本最简单最方便的动画,自然也是限制最多的一种。它仅能使用几种内置的方式进行变换,包括:平移、缩放、透明度、旋转,没了。允许使用插值器(非线性动画)。

ViewPropertyAnimator 的使用非常简单:

view.animate() // 返回一个 ViewPropertyAnimator 对象
  .translationX(200.dp)
  .rotation(90f)
  .setDuration(2000)
  .setStartDelay(1000)

ObjectAnimator

如果想修改的属性不是 ViewPropertyAnimator 内置的怎么办? 比如有一个自定义 View,里面有一个自定义属性。ObjectAnimator 可以满足这个需求。它的原理以字符串方式给定一个属性,通过反射机制寻找对应的 setter 方法,高频调用它不断赋值进而实现动画。

如果是我们自己写的 View 要注意在每一个影响显示的属性的 setter 中调用 invalidate() 重绘,否则即使属性修改了也不会刷新显示。

我们知道屏幕的刷新率很高,如果每一帧都重绘所有组件开销会比较大,所以只会重绘标记为失效的部分,这就是这个方法名的由来。invalidate() 本身不会触发重绘,而是把自己标记为失效,下一次刷新时系统才会真正重绘。

ObjectAnimator 使用也不复杂:

ObjectAnimator.ofFloat(view,"radius",150.dp).apply { 
  startDelay = 1000
}.start()

上面的代码会读取 view.radius 当前的值,然后不断调用 view.setRadius(newValue) 直到设置的目标值 150.dp。在动画期间的任意时刻,值由起止值、动画时长与插值器计算得出。

如果属性的值也是一个自定义对象呢? ObjectAnimator.ofObject() 允许我们使用 TypeEvaluator 实现自己的算法,它通过动画完成度计算出实际的值,比较通用的算法是 startValue + fraction * (endValue - startValue)

AnimatorSet

AnimatorSet 支持整合多个对象,每个对象多个动画,属于宏观管理。

想要多个对象多个动画同时/依次执行怎么办? 当然,可以使用动画监听器一点点做,如果不嫌麻烦的话。更好的选择是使用 AnimatorSet,它可以把多个 ObjectAnimator 合并为一个对象,统一控制延时等属性,也可以让他们按顺序执行,使用方法比较水到渠成:

val bottomFlipAnimator = ObjectAnimator.ofFloat(view, "bottomFlip", 60f)
val flipRotationAnimator = ObjectAnimator.ofFloat(view, "flipRotation", 270f)
AnimatorSet().apply {
  playSequentially(bottomFlipAnimator, flipRotationAnimator, topFlipAnimator) 
}.start()

Keyframe 与 PropertyValuesHolder

PropertyValuesHolder 可以把一个对象的多个属性操作整合成一个动画。内部动画只能并行执行。

Keyframe 就是关键帧,它可以定义当动画执行到某时刻时,具体值应该是多少。比如我们希望一个平移动画,前后两段比较快,中间比较慢,就可以这么定义关键帧:

val length = 200.dp
// 第一个参数是动画的时间完成度(百分比),第二个参数是值,不是百分比!
val keyframe1 = Keyframe.ofFloat(0f, 0f)
val keyframe2 = Keyframe.ofFloat(0.2f, 0.4f * length) // 20% 的时间移动 40% 距离
val keyframe3 = Keyframe.ofFloat(0.8f, 0.6f * length) // 40% 的时间移动 20% 距离
val keyframe4 = Keyframe.ofFloat(1f, 1f * length) // 最后 20% 时间再移动 40% 距离

然后用 PropertyValuesHolder 把这些参数合并成一个动画并播放:

val keyframeHolder = PropertyValuesHolder.ofKeyframe("translationY", keyframe1, keyframe2, keyframe3, keyframe4)
ObjectAnimator.ofPropertyValuesHolder(view, keyframeHolder).apply {
  startDelay = 1000
  duration = 1000
}.start()

Interpolator

关键帧可以做出比较灵活的效果,但过渡比较生硬。Interpolator 则可以实现平滑的非线性动画,它通过时间完成度计算动画完成度。

Android 提供了不少 Interpolator,比较常用的是:

  • AccelerateDecelerateInterpolator: 先加速再减速。适用于变换场景,例如在可视区域里缩放、移动等。
  • DecelerateInterpolator: 一直减速。适用于入场动画,例如 View 飞入屏幕。
  • AccelerateInterpolator: 一直加速。适用于出场动画,例如 View 飞出屏幕。

硬件加速与离屏缓冲

硬件加速使用 GPU 绘制,但是 GPU 只能支持常见的、基本的图形,Android 文档 给出了具体支持的 API 列表,不在列表中的 API 无法在硬件加速开启的时候使用。

离屏缓冲听起来比较高级,其实之前 Xfermode 里使用的 Canvas.saveLayer() 就是一种离屏缓冲。它创建出一个单独像素区域,绘制完毕后在一次性放到屏幕上。不过 Canvas.saveLayer() 创建的缓冲只在当前绘制过程生效,可想而知开销比较大。

后来有了一个新的 API View.setLayerType(),参数的可选值有三个:

  • LAYER_TYPE_HARDWARE:开启 View 级别的离屏缓冲,使用硬件绘制
  • LAYER_TYPE_SOFTWARE:开启 View 级别的离屏缓冲,使用软件绘制
  • LAYER_TYPE_NONE:关闭 View 级别的离屏缓冲。

注意:LAYER_TYPE 的选择不影响硬件加速的开启与否,只是能间接关闭硬件加速,但不能间接开启。

Android 只允许在 App 或 Activity 级别控制硬件加速开关。如果想针对某个 View 关闭硬件加速,给它设置一个软件绘制的离屏缓冲,就相当于这个 View 使用软件绘制了。但倒过来,给它设置硬件离屏缓冲,但 manifests 里关闭了总开关,则不会生效,继续使用软件绘制。

View.setLayerType() 应该在 onDraw() 之外调用,通常是初始化时。与 Canvas.saveLayer() 不同,新的 API 对绘制来讲没有明显的使用场景,仅有的推荐用途在动画 API 上调用 withLayer()

view.animate()
  .translationX(200.dp)
  .withLayer()

对于一些自带的基本属性的变换,Android 内部有优化,它生成的 GPU 指令不会改变,此时开启硬件离屏缓冲就可以优化性能(GPU 不需要重绘,只是在显示到屏幕的时候调整一下位置就行了)。