属性动画和硬件加速

219 阅读11分钟

ViewPropertyAnimator

使⽤ View.animate() 创建对象,以及使⽤ ViewPropertyAnimator.translationX() 等 ⽅法来设置动画; 可以连续调⽤来设置多个动画; 可以⽤ setDuration() 来设置持续时间; 可以⽤ setStartDelay() 来设置开始延时; 以及其他⼀些便捷⽅法。

ObjectAnimator

使⽤ ObjectAnimator.ofXxx() 来创建对象,以及使⽤ ObjectAnimator.start() 来主动启动动画。它的优势在于,可以为⾃定义属性设置动画。

ObjectAnimator animator = ObjectAnimator.ofObject(view, "radius",
Utils.dp2px(200));

另外,⾃定义属性需要设置 getter 和 setter ⽅法,并且 setter ⽅法⾥需要调⽤ invalidate() 来 触发重绘(因为其实每一帧都是一个页面):

public float getRadius() {
return radius;
}

public void setRadius(float radius) {
    this.radius = radius;
    invalidate();
}

可以使⽤ setDuration() 来设置持续时间;可以⽤ setStartDelay() 来设置开始延时;以及其他⼀些便捷⽅法。

完整示例:

class CustomView(context: Context, attrs: AttributeSet) : View(context, attrs) {

    private var scaleX = 1f
    private var scaleY = 1f
    private var alpha = 1f

    init {
        // 获取自定义属性
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
        alpha = typedArray.getFloat(R.styleable.CustomView_alpha, 1f)
        scaleX = typedArray.getFloat(R.styleable.CustomView_scaleX, 1f)
        scaleY = typedArray.getFloat(R.styleable.CustomView_scaleY, 1f)
        typedArray.recycle()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.save()
        // 设置透明度和缩放
        canvas.scale(scaleX, scaleY)
        paint.alpha = (alpha * 255).toInt()  // alpha 范围是 0 到 255
        // 绘制图形
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
        canvas.restore()
    }

    // 用于启动自定义属性动画
    fun startCustomAnimation() {
        // 透明度动画
        val alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f)
        alphaAnimator.duration = 1000

        // 缩放动画
        val scaleXAnimator = ObjectAnimator.ofFloat(this, "scaleX", 1f, 2f)
        val scaleYAnimator = ObjectAnimator.ofFloat(this, "scaleY", 1f, 2f)
        scaleXAnimator.duration = 1000
        scaleYAnimator.duration = 1000

        // 并行播放动画
        val animatorSet = AnimatorSet()
        animatorSet.playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator)
        animatorSet.start()
    }

    // 自定义 getter 和 setter 方法
    fun setAlpha(value: Float) {
        alpha = value
        // 必须调用
        invalidate()
    }

    fun setScaleX(value: Float) {
        scaleX = value
        // 必须调用
        invalidate()
    }

    fun setScaleY(value: Float) {
        scaleY = value
        // 必须调用
        invalidate()
    }
}

自定义属性的 XML 配置

res/values/attrs.xml 文件中定义自定义属性,允许用户在 XML 中使用这些属性。

<declare-styleable name="CustomView">
    <attr name="alpha" format="float"/>
    <attr name="scaleX" format="float"/>
    <attr name="scaleY" format="float"/>
</declare-styleable>

在布局 XML 中使用自定义视图

在布局文件中使用 CustomView,并设置这些自定义属性。

<com.example.myapp.CustomView
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:alpha="0.5"
    app:scaleX="1.5"
    app:scaleY="1.5"/>

启动动画

在页面中中调用 startCustomAnimation() 方法来启动动画。

val customView = findViewById<CustomView>(R.id.customView)
customView.startCustomAnimation()

另外,可以直接在自定义视图中控制属性动画,并在代码中设置动画,而不需要在 attrs.xml 中定义任何属性。这样做的好处是,可以完全控制动画的行为,而不受 XML 中的约束。

Interpolator

插值器,⽤于设置时间完成度到动画完成度的计算公式,直⽩地说即设置动画的速度曲线,通过 setInterpolator(Interpolator) ⽅法来设置。 常⽤的有 AccelerateDecelerateInterpolator AccelerateInterpolator DecelerateInterpolator LinearInterpolator 。

上边的实例调整后:

class CustomView(context: Context) : View(context) {

    private var scaleX = 1f
    private var scaleY = 1f
    private var alpha = 1f

    init {
        // 初始化视图时,可以设置默认的属性值
        scaleX = 1f
        scaleY = 1f
        alpha = 1f
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.save()
        // 使用当前的缩放和透明度属性
        canvas.scale(scaleX, scaleY)
        paint.alpha = (alpha * 255).toInt()  // alpha 范围是 0 到 255
        // 绘制图形
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
        canvas.restore()
    }

    // 启动自定义动画
    fun startCustomAnimation() {
        // 透明度动画
        val alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f)
        alphaAnimator.duration = 1000

        // 缩放动画
        val scaleXAnimator = ObjectAnimator.ofFloat(this, "scaleX", 1f, 2f)
        val scaleYAnimator = ObjectAnimator.ofFloat(this, "scaleY", 1f, 2f)
        scaleXAnimator.duration = 1000
        scaleYAnimator.duration = 1000

        // 并行播放动画
        val animatorSet = AnimatorSet()
        animatorSet.playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator)
        animatorSet.start()
    }

    // 自定义 getter 和 setter 方法
    fun setAlpha(value: Float) {
        alpha = value
        invalidate()
    }

    fun setScaleX(value: Float) {
        scaleX = value
        invalidate()
    }

    fun setScaleY(value: Float) {
        scaleY = value
        invalidate()
    }
}
  • XML 不必定义自定义属性:可以完全通过代码来处理动画和视图的属性。

  • 直接在代码中处理动画:使用 ObjectAnimator, ValueAnimator, 或其他动画类,直接操作视图的属性。

  • 灵活性:不依赖于 XML 属性定义,使得视图和动画的行为更加灵活,但也牺牲了一部分通过 XML 配置的简便性。

PropertyValuesHolder

⽤于设置更加详细的动画,例如多个属性应⽤于同⼀个对象:

PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("radius",
Utils.dp2px(200));
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("offset",
Utils.dp2px(100));
ObjectAnimator animator =
PropertyValuesHolder.ofPropertyValuesHolder(view, holder1, holder2);

或者,配合使⽤ Keyframe ,对⼀个属性分多个段:

Keyframe keyframe1 = Keyframe.ofFloat(0, Utils.dpToPixel(100));
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, Utils.dpToPixel(250));
Keyframe keyframe3 = Keyframe.ofFloat(1, Utils.dpToPixel(200));
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("radius",
keyframe1, keyframe2, keyframe3);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
holder);

AnimatorSet

将多个 Animator 合并在⼀起使⽤,先后顺序或并列顺序都可以:

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animator1, animator2);
animatorSet.start();

TypeEvaluator

⽤于设置动画完成度到属性具体值的计算公式。默认的 ofInt() ofFloat() 已经有了⾃带的 IntEvaluator FloatEvaluator ,但有的时候需要⾃⼰设置 Evaluator。例如,对于颜⾊,需要 为 int 类型的颜⾊设置 ArgbEvaluator,⽽不是让它们使⽤ IntEvaluator:

animator.setEvaluator(new ArgbEvaluator());

如果你对 ArgbEvaluator 的效果不满意,也可以⾃⼰写⼀个 HsvEvaluator :

public class HsvEvaluator implements TypeEvaluator<Integer> {
    @Override
    public Object evaluate(float fraction, Object startValue, Object
endValue) {

   }
}

另外,对于不⽀持的类型,也可以使⽤ ofObject() 来在创建 Animator 的同时就设置上Evaluator,⽐如 NameEvaluator :

public class NameEvaluator implements TypeEvaluator<String> {
    List<String> names = ...;
    @Override
    public String evaluate(float fraction, String startValue, String
endValue) {
        if (!names.contains(startValue)) {
            throw new IllegalArgumentException("Start value not
existed");
       }
        if (!names.contains(endValue)) {
            throw new IllegalArgumentException("End value not existed");
       }
        int index = (int) ((names.indexOf(endValue) -
names.indexOf(startValue)) * fraction);
        return names.get(index);
   }
}
ObjectAnimator animator = ObjectAnimator.ofObject(view, "name", new
NameEvaluator(), "Jack");

Listeners

和 View 的点击、⻓按监听器⼀样,Animator 也可以使⽤ setXxxListener() addXxxListener () 来设置监听器。 这是最基本的 Animator,它不和具体的某个对象联动,⽽是直接对两个数值进行渐变计算。使⽤很少。

硬件加速

  • 使⽤ CPU 绘制到 Bitmap,然后把 Bitmap 贴到屏幕,就是软件绘制;
  • 使⽤ CPU 把绘制内容转换成 GPU 操作,交给 GPU,由 GPU 负责真正的绘制,就叫硬件绘制。使用GPU 绘制就叫做硬件加速.GPU 绘制简单图形(例如⽅形、圆形、直线)在硬件设计上具有先天优势,会更快流程得到优化(重绘流程涉及的内容更少)。可能存在兼容性问题,由于使⽤ GPU 的绘制(暂时)⽆法完成某些绘制,因此对于⼀些特定的 API,需要关闭硬件加速来转回到使⽤ CPU 进⾏绘制。

image.png

离屏缓冲

离屏缓冲是指为一个 View(或 View 的一部分)单独分配一个绘制区域,将其绘制内容先写入这个缓冲区,而不是直接绘制到屏幕上。这样可以避免影响当前屏幕上的显示,常用于提高性能或实现特殊的绘制效果。 当使用 LAYER_TYPE_HARDWARE 时,系统利用 GPU 直接在主画布(屏幕)上绘制内容,避免了离屏缓冲带来的额外开销(如内存分配和数据拷贝)

setLayerType()saveLayer()

  1. setLayerType()
    该方法是设置整个 View 的离屏缓冲,而不是单独针对 onDraw() 中的某个绘制过程。

    • 如果使用 LAYER_TYPE_SOFTWARE 参数,它会为 View 创建一个离屏缓冲区(通常是一个 Bitmap),并通过软件渲染进行绘制,实际上会关闭硬件加速。

    • 需要注意,setLayerType() 的作用不仅是切换硬件加速,它实际是为 View 设置一个离屏缓冲,让后续的绘制操作写入该缓冲区。

setLayerType() 参数详解:

LAYER_TYPE_NONE - 默认类型,使用系统当前的绘制方式(通常是硬件加速)。

LAYER_TYPE_HARDWARE - 强制启用硬件加速,所有绘制操作都在 GPU 上完成。适用于需要高性能图形渲染的场景。

LAYER_TYPE_SOFTWARE - 使用软件渲染代替硬件加速,在离屏缓冲区(Bitmap)上完成所有绘制操作。虽然性能较低,但在某些硬件加速不支持的特定效果(如复杂的路径操作)下,可以使用它作为替代方案。

  1. saveLayer()
    saveLayer() 是针对 Canvas 的方法,通常用于 onDraw() 中,当希望某一部分绘制使用离屏缓冲时,可以使用它。

    • 每次调用 onDraw() 时都会触发 saveLayer(),因此使用不当会带来性能开销。因此,官方文档建议,除非有特殊需求,应该优先使用 setLayerType() 来代替 saveLayer(),因为后者在性能上较为消耗。
class CustomView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    private val paint = Paint().apply {
        color = Color.RED
        style = Paint.Style.FILL
    }

    private val rect = RectF(100f, 100f, 300f, 300f)

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        // 保存当前 Canvas 状态,并启用离屏缓冲
        val saveLayerId = canvas.saveLayer(rect, null)

        // 在离屏缓冲中绘制一个矩形
        paint.color = Color.RED
        canvas.drawRect(rect, paint)

        // 在离屏缓冲中绘制一个半透明圆,覆盖部分矩形
        paint.color = Color.BLUE
        paint.alpha = 128
        canvas.drawCircle(200f, 200f, 100f, paint)

        // 恢复到主画布,将离屏缓冲的内容合并到主画布上
        canvas.restoreToCount(saveLayerId)
    }
}

代码解析:

  • saveLayer 的使用

    • canvas.saveLayer(rect, null):创建一个离屏缓冲区,绘制范围为指定的 rect。离屏缓冲中执行的所有绘制操作不会立即反映到主画布上。
  • 绘制操作

    • 在离屏缓冲中绘制一个红色矩形。
    • 再绘制一个半透明蓝色圆,部分覆盖矩形。
  • 合并离屏缓冲内容

    • canvas.restoreToCount(saveLayerId):将离屏缓冲区中的内容合并到主画布上,并恢复到保存的 Canvas 状态。
  • 主画布上会显示一个红色矩形和部分重叠的半透明蓝色圆。

  • 使用 saveLayer 的好处是,矩形和圆的绘制过程不会直接影响主画布。

小结

  • setLayerType() 用于为整个 View 设置离屏缓冲并关闭硬件加速,通常用来处理 View 层级的离屏绘制。
  • saveLayer() 用于指定某一部分绘制使用离屏缓冲,适用于 onDraw() 中,但它可能会带来较大的性能开销,因此不推荐频繁使用。
setLayerType(LAYER_TYPE HARDWARE, null) // 开启离屏缓冲,并使用硬件来实现
setLayerType(LAYER_TYPE SOFTWARE, null) // 开启离屏缓冲,并使用软件来实现
setLayerType(LAYER TYPE NONE,null) // 关闭离屏缓冲

在安卓开发中,如果使用离屏缓冲(Offscreen Buffer),相当于对 View 的渲染流程进行了间接优化。开启离屏缓冲后,View 不再直接被绘制到屏幕上,而是先绘制到缓冲区(buffer)中,然后再将缓冲区的内容贴到屏幕上显示。这种方式虽然间接,但在某些场景下非常接近硬件加速的效果,尤其是动画过程中。

通过使用离屏缓冲,可以临时优化动画的性能。例如,在动画过程中(从动画开始到结束),可以临时开启离屏缓冲,动画结束后再关闭。这种操作的意义在于可以显著提高动画过程中 View 的渲染效率。以下是具体原理和场景说明:

1. 动画过程中开启离屏缓冲的作用

  1. 优化动画渲染效率:
    在动画执行过程中,像 translation(平移)、alpha(透明度)、scale(缩放)、rotation(旋转)等 Android 自带的属性可以获得系统级的 GPU 优化。这些属性的变化不会导致 GPU 操作指令(GPU Operations)的改变,而只需要对已有指令做轻微调整。
  2. 避免重新绘制:
    GPU 只需调整这些属性对应的绘制指令,而无需重新生成内容或进行重绘。例如,当一个 View 向右平移 200 像素,GPU 只需在渲染时调整位置,不会触发整个 View 的重新绘制。
  3. 离屏缓冲的开销:
    在动画开始时,离屏缓冲的初始化可能较为耗时,因为需要为缓冲区分配额外的内存。但一旦缓冲区创建完成,后续的动画帧无需重新绘制内容,GPU 可以直接使用缓冲区数据进行渲染,从而提升性能。

2. 离屏缓冲的限制

  1. 仅对系统优化的属性有效:
    translationXtranslationYalphascaleXscaleYrotation 等系统原生属性会被优化,但自定义属性无法享受这种优化。例如:

    • 自定义的圆角半径(radius)或边框宽度(strokeWidth)等属性。
    • 自定义属性的变化需要完整的重绘流程,因此无法从离屏缓冲中受益。
  2. 离屏缓冲的额外开销:
    在离屏缓冲首次创建时,需要分配缓冲区并进行绘制操作,这些步骤会消耗一定的性能和内存。如果动画使用的属性不符合优化条件,反而可能导致整体性能下降。


3. 使用 setLayerType 优化动画

在实际开发中,可以通过 setLayerType 方法开启或关闭硬件加速或离屏缓冲:

// 动画开始时启用硬件层
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);

// 启动动画
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 200f);
animator.setDuration(1000);
animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        // 动画结束时恢复默认层
        view.setLayerType(View.LAYER_TYPE_NONE, null);
    }
});
animator.start();

4. 离屏缓冲的原理

当动画属性发生改变时,GPU 会在渲染到屏幕的瞬间进行调整,而无需改变绘制内容。GPU 通过以下方式实现高效渲染:

  • 仅调整缓冲区的位置、大小或透明度。
  • 避免每一帧重新生成绘制指令或绘制内容。

例如,translationX 的变化只需调整缓冲区的位置即可,而 radius 等自定义属性则需要完整重绘。

5. 注意事项

  1. 离屏缓冲适合高频更新动画,比如位移、缩放、透明度变化等。
  2. 如果使用自定义属性动画,慎用离屏缓冲,否则可能降低性能。
  3. 在性能调优时,可以结合工具(如 Profile GPU Rendering)分析动画帧率,判断是否需要启用离屏缓冲。