RenderThread 是在 Android Lollipop 中新加入的组件。关于该组件的文档描述甚少,仅有一个模糊的定义:
RenderThread 是一个由系统管理的线程。较之 UI 线程的延迟,RenderThread 的动画播放更为流畅。
为了解 RenderThread 的工作机制,需要介绍一些基本的概念。
当启动硬件加速后,Android 使用 “display list” 组件进行绘制而非直接使用 CPU 绘制每一帧。Display List 是一系列绘制操作的记录,抽象为 RenderNode
类。
这样间接的进行绘制操作的优点颇多:
- Display List 可以按需多次绘制而无须同业务逻辑交互。
- 特定的绘制操作(如 translation, scale 等)可以作用于整个 display list 而无须重新分发绘制操作。
- 当知晓了所有绘制操作后,可以针对其进行优化:例如,所有的文本可以一起进行绘制一次。
- 可以将对 display list 的处理转移至另一个线程(非 UI 线程)。
最后一点恰好是 RenderThread 负责的:在 UI 线程外执行优化操作与将绘制操作分发给 GPU。
在 Lollipop 之前,执行“重”操作的同时对 View 属性执行动画(例如在 Activity 间执行 transition 动画)几乎不可能。而 Lollipop 及以上的 Android 版本,这类动画以及其它一些效果(例如 ripple )却可以流畅的执行。这样的黑科技便源自 RenderThread 的帮助。
渲染工作的真正执行者是 GPU,而 GPU 对于动画一无所知:执行动画的唯一途径便是将每一帧的不同绘制操作分发给 GPU,但该逻辑本身不能在 GPU 上执行。而如果在 UI 线程执行该操作,任意的重操作都将阻塞新的绘制指令及时分发,于是动画便出现了延迟。
如前文所述,RenderThread 可以处理 display list 流程的某些部分,但要注意到 display list 的创建和修改仍需要在 UI 线程中执行。
那么动画是怎样从不同的线程中进行更新的呢?
开启硬件加速后,Canvas
由 DisplayListCanvas
类实现,该类重载了部分绘制方法,方法参数类型使用 CanvasProperty
对象替换原有的基本类型(例如用 CanvasProperty<Float>
替换 float
类型),CanvasProperty
是原有类型的包装类。这样,dislplay list 及其对应的绘制操作便可以在 UI 线程中创建,而绘制方法的参数可以通过 CanvasProperty
的映射动态地、通过 RenderThread 异步地修改。
实现上述操作还需一步:CanvasProperty
的值需要通过 RenderNodeAnimator
执行动画操作,RenderNodeAnimator
中包含了动画的配置及初始值。
此类动画有一些有趣的特性:
- 目标
DisplayListCanvas
需要手动设置且不可修改 - 该动画发送后无须关注:动画开始执行后只能取消(没有暂停和恢复)且无法知晓此刻的属性值
- 可以设置自定义插值器,插值器将在 RenderThread 执行
- start delay 一般在 RenderThread 上等待
时至今日,可以通过 RenderThread 执行的动画如下:
View 属性 (通过 View 的 animate
方法执行)
- Translation (X, Y, Z)
- Scale (X, Y)
- Rotation (X, Y)
- Alpha
- Circular reveal animation (通过
ViewAnimationUtils
的createCircularReveal
执行)
Canvas 方法与 Canvas 属性
- drawCircle(centerX, centerY, radius, paint)
- drawRoundRect(left, top, right, bottom, cornerRadiusX, cornerRadiusY, paint)
Paint 属性
- Alpha
- Stroke width
似乎 Google 仅将 Material Design 动画中所需的绘制操作进行了封装。
在 Android N 及以上,RenderThread 的能力的得以拓展(例如,AnimatedVectorDrawable
将使用 RenderThread 执行动画),也许今后 RenderThread 会成为开放 API。
简而言之,我可以在 RenderThread 执行动画吗?
依据文档,不可以。 除非使用 View.animate
或是 ViewAnimationUtils.createCircularReveal
。
通过 Hack ,可以实现。 对于未开放的组件,我们都可以通过反射获取对相关类、方法的引用,通过包装类以保证类型安全,反射失败时提供反馈等等。
笔者实现了使用 RenderThread 执行动画的库。
使用该库十分简单,仅需 3 步:
CanvasProperty<Float> centerXProperty;
CanvasProperty<Float> centerYProperty;
CanvasProperty<Float> radiusProperty;
CanvasProperty<Paint> paintProperty;
Animator radiusAnimator;
Animator alphaAnimator;
@Override
protected void onDraw(Canvas canvas) {
if (!animationInitialised) {
// 1. create as many CanvasProperty as needed with the initial animation values
centerXProperty = RenderThread.createCanvasProperty(canvas, initialCenterX);
centerYProperty = RenderThread.createCanvasProperty(canvas, initialCenterY);
radiusProperty = RenderThread.createCanvasProperty(canvas, initialRadius);
paintProperty = RenderThread.createCanvasProperty(canvas, paint);
// 2. create one or more Animator with the properties you want to animate
radiusAnimator = RenderThread.createFloatAnimator(this, canvas, radiusProperty, targetRadius);
alphaAnimator = RenderThread.createPaintAlphaAnimator(this, canvas, paintProperty, targetAlpha);
radiusAnimator.start();
alphaAnimator.start();
}
// 3. draw to the Canvas
RenderThread.drawCircle(canvas, centerXProperty, centerYProperty, radiusProperty, paintProperty);
}
复制代码