Compose 的绘制和原生的差异
原生的 View 绘制我们都很清楚可以在自定义 View 的 onDraw() 通过 Canvas 实现:
class CustomView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint()
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
paint.color = Color.Yellow
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
}
Compose 的绘制底层其实还是使用的 Canvas,不过相比原生的自定义绘制,Compose 提供了更上层的 API,这套 API 能让我们更简单更直接实现和原生一样的效果。
我们在讲解 Modifier 时就有提及 DrawModifier 并对其进行了详细的分析,所有的绘制步骤包括组件自身的绘制、手写的自定义绘制都是在 DrawModifier。我们可以用 DrawModifier 实现不同的绘制顺序。
比如 Modifier.drawBehind() 就比较适合绘制背景:
@Preview
@Composable
fun CustomText() {
Text("test", Modifier.drawBehind {
// 相当于原生绘制把 drawRect() 和 drawColor() 合并的效果
// 并且不再需要提供 Paint 控制参数,参数配置都分配到独立的组件
drawRect(Color.Yellow)
})
}
上面是一个文本,然后使用 Modifier.drawBehind() 绘制黄色的背景。可以发现使用 Compose 绘制和原生不同的是,我们不需要指定绘制范围,默认就是文本范围。这其实是 Compose 默认提供的就是组件的范围:
DrawScope.kt
fun drawRect(
color: Color,
topLeft: Offset = Offset.Zero, // 矩形左上角
size: Size = this.size.offsetSize(topLeft), // 矩形范围
alpha: Float = 1.0f,
style: DrawStyle = Fill,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
)
我们在讲解 DrawModifier 时有提到 Compose 提供了 Modifier.drawWithContent() 让我们可以自定义绘制的顺序:
@Preview
@Composable
fun CustomText() {
Text("test", Modifier.drawWithContent {
// 原有内容绘制前的处理...
drawRect(Color.Yellow)
drawContent() // 绘制原有内容
// 原有内容绘制后的处理...
drawLine(Color.Red, Offset(0f, size.height / 2), Offset(size.width, size.height / 2), 2.dp.toPx())
})
}
如果想要完全自定义的绘制,可以使用 Box() 加上 Modifier.drawBehind(),或者用 Compose 提供的 Canvas,两者的效果是等价的:
setContent {
// 两种方式是等价的,只是用 Canvas 更直观一些
Box(Modifier.size(80.dp).drawBehind {
})
Canvas(Modifier.size(80.dp)) {
}
}
Box.kt
@Composable
fun Box(modifier: Modifier) {
Layout({}, measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier)
}
Canvas.kt
@Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
Spacer(modifier.drawBehind(onDraw)) // Spacer 的处理和 Box 几乎一致
Spacer.kt
@Composable
fun Spacer(modifier: Modifier) {
Layout({}, modifier) { _, constraints ->
with(constraints) {
val width = if (hasFixedWidth) maxWidth else 0
val height = if (hasFixedHeight) maxHeight else 0
layout(width, height) {}
}
}
}
简单使用 Canvas 实现绘制一个图片旋转的效果:
@Preview
@Composable
fun CustomImage() {
val image = ImageBitmap.imageResource(R.drawable.avatar)
Canvas(Modifier.size(100.dp)) {
// 旋转相关的都在 rotate() 范围,不需要手动代码旋转回来
// 如果是原生需要你手动处理旋转后再恢复
// 平面旋转
rotate(30f) {
drawImage(image, dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt()))
}
}
}
所以 Compose 自定义绘制主要有三种方式:
-
Modifier.drawBehind():适用于背景绘制
-
Modifier.drawWithContent():适用于需要控制绘制顺序的场景
-
Box() 和 Modifier.drawBehind() 或 Canvas():适用于需要处理复杂绘制的场景
Compose 复杂绘制降回原生 Canvas
如果是围绕一条坐标轴旋转的,Compose 可以用 Modifier.graphicsLayer() 间接的方式实现:
@Preview
@Composable
fun CustomImage() {
val image = ImageBitmap.imageResource(R.drawable.avatar)
// graphicsLayer 控制沿着纵轴或横轴旋转
// 但是一起设置的话就不是我们想要的效果,所以它只能设置沿着一个轴向旋转
Canvas(Modifier.size(100.dp).graphicsLayer { rotationX = 45f }) {
drawImage(image, dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt()))
}
}
但少数情况下 Compose 没有提供的相应的 API,比如说三维旋转效果,沿着多条方向的 Compose 就没有提供 API 了,需要我们自己降回原生 API 即使用 Canvas 实现:
@Preview
@Composable
fun CustomImage() {
val image = ImageBitmap.imageResource(R.drawable.avatar)
val paint by remember { mutableStateOf(Paint()) }
val camera by remember { mutableStateOf(Camera()) }
val rotateAnimatable = remember { Animatable(0f) }
LaunchedEffect(Unit) {
rotateAnimatable.animateTo(360f, infiniteRpeatable(tween(2000))
}
Canvas(Modifier.size(100.dp)) {
// 使用 Compose 的 Canvas 实现复杂的绘制效果
drawIntoCanvas {
// it.translate() 配合的 camera.rotateX() 实现三维 x 轴旋转正确
// it.rotate() 实现沿着对角线旋转
it.translate(size.width / 2, size.height / 2)
it.rotate(-45f)
camera.save()
camera.rotateX(rotationAnimable.value)
// nativeCanvas 下沉到原生的 Canvas
camera.applyToCanvas(it.nativeCanvas)
camera.restore()
it.rotate(-45f)
it.translate(-size.width / 2, -size.height / 2)
it.drawImageRect(image, dstSize = IntSize(size.width.roundToInt(),
size.height.roundToInt()), paint = paint)
}
}
}