一、概念
- 所有的绘制操作都是通过调整像素大小来执行的。若要确保项目在不同的设备密度和屏幕尺寸上都能采用一致的尺寸,请务必使用 .toPx() 对 dp 进行转换或者采用小数尺寸。
二、Modifier 修饰符绘制
官方页面
在修饰的可组合项之上或之下绘制。
.drawWithContent | fun Modifier.drawWithContent( 在 Lambda 中调用 drawContent() 就是绘制所修饰的内容,由此控制先后顺序,后绘制的会显示在上面。 |
.drawBehind | fun Modifier.drawBehind( 修饰的内容会显示在 Lambda 内容之上(底层是先绘制 Lambda 内容再绘制所修饰的内容,后绘制的会显示在上面)。 |
.drawWithCache | fun Modifier.drawWithCache( 当绘制复杂效果时,不希望因为重组而重新创建 Lambda 中用于绘制的实例如 Bush、Path 等,这可能会产生内存抖动。在 Lambada 中调用 onDrawWithContent()、onDrawBehind() 就类似于上面两个修饰符的功能。 |
@Composable
fun Demo() {
Row(
modifier = Modifier.size(150.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painterResource(id = R.drawable.logo_wechat_square),
contentDescription = null,
modifier = Modifier
.size(50.dp)
.drawWithContent {
drawContent()
drawRedDot() //后绘制的会显示在上面
}
)
Image(
painterResource(id = R.drawable.logo_wechat_square),
contentDescription = null,
modifier = Modifier.padding(start = 10.dp).size(50.dp).drawBehind {
drawRedDot()
}
)
}
}
fun DrawScope.drawRedDot() {
drawCircle(
color = Color.Red,
radius = 18F,
center = Offset(drawContext.size.width, 0f)
)
}
三、Canvas() 可组合项绘制
是一个可组合项。Compose 作为跨平台 UI 框架,所使用的 Canvas() 函数只是一个封装,最终还是调用具体平台即 Android 原生的 Canvas。
fun Canvas( modifier: Modifier, onDraw: DrawScope.() -> Unit ) = Spacer(modifier.drawBehind(onDraw)) 发现该方法只是一个封装,真正绘制的是调用 drawBehind()。绘制内容是显示在 Spacer 下面的,由于 Spacer 是透明的,因此我们所绘制内容得以全部显示。 |
四、Brush
官方页面
用于绘制颜色(只指定一种颜色就是纯色)。
linearGradient 线性渐变 | fun linearGradient( 水平渐变和垂直渐变底层就是调用的线性渐变。 | |
horizontalGradient 水平方向渐变 | fun horizontalGradient( colors: List<Color>, startX: Float = 0.0f, endX: Float = Float.POSITIVE_INFINITY, tileMode: TileMode = TileMode.Clamp ): Brush | |
verticalGradient 垂直方向渐变 | fun verticalGradient( colors: List<Color>, startY: Float = 0.0f, endY: Float = Float.POSITIVE_INFINITY, tileMode: TileMode = TileMode.Clamp ): Brush | |
radialGradient 放射渐变 | fun radialGradient( colors: List<Color>, center: Offset = Offset.Unspecified, //中心位置 radius: Float = Float.POSITIVE_INFINITY, //半径 tileMode: TileMode = TileMode.Clamp ): Brush | |
sweepGradient 扫描渐变 | fun sweepGradient( colors: List<Color>, center: Offset = Offset.Unspecified ): Brush |
4.2.1 使用 colorStop 更改颜色分布
自定义颜色在渐变中的显示方式,可以调整每种颜色的 colorStop 值,0 ~ 1 之间的小数。
val colorStops = arrayOf(
0.0f to Color.Yellow,
0.2f to Color.Red,
1f to Color.Blue
)
Box(modifier = Modifier
.requiredSize(200.dp)
.background(Brush.horizontalGradient(colorStops = colorStops))
)
4.2.2 使用 TileMode 让图案重复显示
当未指定 Brush 的开始位置 start 和结束位置 end 时,默认会填满整个区域,只有在区域 > Brush 时 TileMode 才会在渐变中平铺。以下举例 HorizontalGradient 的效果。
TileMode.Repeated | 将区域剩余空间绘制为重复的顺序颜色。 | |
TileMode.Mirror | 将区域剩余空间绘制为重复的反转颜色。 | |
TileMode.Clamp | 将区域剩余空间绘制为结束颜色。 | |
TileMode.Decal | 将区域剩余空间绘制为透明色。(仅适用于 API 31 及更高版本。可使用 TileMode.isSupported() 确定设备是否支持 TileMode。如果使用了不受支持的 TileMode,系统会应用默认的 TileMode.Clamp。) |
4.2.3 更改 Brush 大小
当知道绘制区域大小时(如在 DrawScope 中通过 size 获取)可以按照 TileMode 方式平铺,在不知道的情况下(如将 Brush 分配给文字)可以扩展 Shader 重写 createShader() 函数利用绘制区域大小 size 形参。对于 radialGradient 如果未指定中心位置 center 和半径radius,渐变将占据整个 DrawScope 但是是以宽高较小的那边为直径,此时自定义大小会获得更好的效果(发散到屏幕外边去)。
val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val customBrush = remember {
object : ShaderBrush() {
override fun createShader(size: Size): Shader {
return LinearGradientShader(
colors = listColors,
from = Offset.Zero,
to = Offset(size.width / 4f, 0f),
tileMode = TileMode.Mirror
)
}
}
}
Box(
modifier = Modifier
.requiredSize(200.dp)
.background(customBrush)
)
4.2.4 使用图片作为 Brush
如需使用 ImageBitmap 作为 Brush,请以 ImageBitmap 的形式加载相应图片,然后创建 ImageShader Brush。可以应用于一下几种类型的绘制:背景、文字、画布。
val imageBrush =
ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.dog)))
//用于 background
Box(
modifier = Modifier
.requiredSize(200.dp)
.background(imageBrush)
)
//用于 TextStyle
Text(
text = "Hello Android!",
style = TextStyle(
brush = imageBrush,
fontWeight = FontWeight.ExtraBold,
fontSize = 36.sp
)
)
//用于 DrawScope#drawCircle()
Canvas(onDraw = {
drawCircle(imageBrush)
}, modifier = Modifier.size(200.dp))
五、DrawScope
官方页面
在 DrawScope 中,可以访问到 drawContext 成员,它存储了以下信息:绘制尺寸size、封装的canvas、用来旋转缩放移动的transform,而通过 canvas.nativeCanvas 就能获取具体平台的实现,即可以调用 Android 原生的 Canvas 来实现更多需求。
绘制 | drawLine 画线 |
drawRect 画矩形 | |
drawRoundRect 画圆角矩形 | |
drawImage 绘制图片 | |
drawCircle 画圆形 | |
drawOval 画椭圆形 | |
drawArc 画弧度跟扇形 | |
drawPath 画路径 | |
drawPoints 画点 | |
行为 | inset 将DrawScope坐标空间平移 |
translate 平移坐标 | |
rotate(旋转坐标)讲的是旋转了多少角度 rotateRad(旋转坐标)讲的是旋转了多少弧度 | |
scale 缩放坐标 | |
clipRect 裁剪矩形区域,绘制在裁剪好的矩形区域内。ClipOp.Difference从当前剪辑中减去提供的矩形。 | |
clipPath 裁剪路径 | |
drawIntoCanvas 直接提供底层画布 | |
withTransform 组合转换 |
5.1 绘制
5.1.1 画线 drawLine()
fun drawLine( | |
cap 线条两头的形状 | StrokeCap.Butt 平的(默认) |
StrokeCap.Square 也是平的但是长一截 | |
StrokeCap.Round 圆的 | |
pathEffect 线条效果 | PathEffect.cornerPathEffect(radius: Float) 将线段之间的锐角替换为指定半径的圆角 radius是半径 |
PathEffect.dashPathEffect(intervals: FloatArray, phase: Float = 0f) 将形状绘制为具有给定间隔的一系列破折号。比如虚线 例如interval={20,5},第一个参数表示虚线的长度是20,5是虚线之间的间隔是5. phase 偏移 | |
PathEffect.chainPathEffect(outer: PathEffect, inner: PathEffect) 创建一个PathEffect,将内部效果应用于路径,然后应用外部效果 | |
PathEffect.stampedPathEffect(shape: Path, advance: Float, phase: Float,style: StampedPathEffectStyle) 用path表示的指定形状冲压绘制的路径. shape要踩踏的路径,advance 每个冲压形状之间的前进间距, phase 在压印第一个形状之前要偏移的相位量, style如何在每个位置转换形状,因为它是冲压. style有三种取值 StampedPathEffectStyle.Translate 平移 ,StampedPathEffectStyle.Rotate 旋转,StampedPathEffectStyle.Morph 变形 |
5.2 行为
5.2.1 缩放 scale()
5.2.2 平移 translate()
5.2.3 旋转 rotate()
5.2.4