一、概念
所有的绘制操作都是通过调整像素大小来执行的。若要确保项目在不同的设备密度和屏幕尺寸上都能采用一致的尺寸,请务必使用 .toPx() 对 dp 进行转换或者采用小数尺寸。
fun Dp.toPx(): Float = value * density |
val width = 50.dp.toPx()
二、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() 可组合项绘制
是一个可组合项,在 DrawScope 作用域中调用各种函数绘制。Compose 作为跨平台 UI 框架,所使用的 Canvas() 函数只是一个封装,最终还是调用具体平台即 Android 原生的 Canvas。
fun Canvas( modifier: Modifier, onDraw: DrawScope.() -> Unit ) = Spacer(modifier.drawBehind(onDraw)) 发现该方法只是一个封装,真正绘制的是调用 drawBehind()。绘制内容是显示在 Spacer 下面的,由于 Spacer 是透明的,因此我们所绘制内容得以全部显示。 |
@Composable
fun Demo() {
Canvas(
modifier = Modifier.size(400.dp).background(Color.White)
) {
//通过 size 可以拿到可以拿画布的宽高
val width = size.width
val height = size.height
//通过 center 可以拿到画布中心点坐标
val center = center
//调用各种方法绘制
drawRect(
color = Color.Blue,
size = Size(width,height)
)
}
}
四、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))
五、BlendMode 混合模式
混合是有个原图像src,一个dst目标图像,这两个图的相交区域的各种组合情况。
Clear | 删除源图像和目标图像,不留下任何内容 |
Src | 删除目标图像,只绘制源图像 |
Dst | 删除源图像,只绘制目标图像 |
SrcOver | 源图和目标图合成,源图在上 |
DstOver | 目标图和源图合成,目标图在上 |
SrcIn | 显示源图和目标图相交的部分,并且只显示源图像 |
DstIn | 显示目标图和源图相交的部分,并且只显示目标图 |
SrcOut | 显示源图像和目标图不相交的部分,并且只显示源图 |
DstOut | 显示目标图和源图像不相交的部分,并且只显示目标图 |
SrcAtop | 显示目标图,并且在相交的地方显示源图 |
DstAtop | 显示源图,并在相交的地方显示目标图 |
Xor | 显示源图和目标图,但相交的位置不显示(代码注释:对源图像和目标图像应用按位异或运算符。这就使得它们重叠的地方保持透明。) |
Plus | 对源映像和目标映像的组件求和。其中一个图像的像素中的透明度降低了该图像对相应输出像素的贡献,就好像该图像中该像素的颜色较暗一样 |
Modulate | 将源图像和目标图像的颜色分量相乘。这只能产生相同或较深的颜色(乘以白色,1.0,结果不变;乘以黑色(0.0,结果为黑色)。合成两个不透明图像时,这与在投影仪上重叠两个透明胶片的效果类似。对于同样乘以alpha通道的变量,请考虑乘以。 |
Screen | 将源图像和目标图像的分量的逆相乘,然后求逆结果。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。这基本上与调制混合模式相同,但是在乘法之前颜色值反转,结果在渲染之前反转回来。这只能产生相同或较浅的颜色(乘以黑色,1.0,结果不变;乘以白色(0.0,结果为白色)。类似地,在alpha通道中,它只能产生更不透明的颜色。这与两台投影仪同时在同一屏幕上显示图像的效果相似。 |
Overlay | 将源图像和目标图像的分量相乘,然后调整它们以支持目标。具体来说,如果目标值较小,则将其与源值相乘,而如果源值较小,则将源值的倒数与目标值的倒数相乘,然后反转结果。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。 |
Darken | 通过从每个颜色通道中选择最低值来合成源图像和目标图像。输出图像的不透明度的计算方法与SrcOver相同。 |
Lighten | 通过从每个颜色通道中选择最高值来合成源图像和目标图像。输出图像的不透明度的计算方法与SrcOver相同。 |
ColorDodge | 将目标除以源的倒数。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。注意这个BlendMode只能在androidapi级别29及以上使用。 |
ColorBurn | 将目标的倒数除以源的倒数,然后求结果的倒数。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。注意这个BlendMode只能在androidapi级别29及以上使用。 |
Hardlight | 将源图像和目标图像的分量相乘,然后调整它们以有利于源图像。具体来说,如果源值较小,则将其与目标值相乘,而如果目标值较小,则将目标值的倒数与源值的倒数相乘,然后反转结果。反转组件意味着完全饱和的通道(不透明白色)被视为值0.0,而通常被视为0.0(黑色,透明)的值被视为1.0。注意这个BlendMode只能在androidapi级别29及以上使用。 |
Softlight | 对于低于0.5的源值,使用ColorDodge;对于高于0.5的源值,使用ColorBurn。这会产生类似的效果,但比叠加效果更柔和。注意这个BlendMode只能在androidapi级别29及以上使用。 |
Difference | 从每个通道的较大值中减去较小的值。合成黑没有效果;合成白色将反转其他图像的颜色。输出图像的不透明度的计算方法与SrcOver相同。注意这个BlendMode只能在androidapi级别29及以上使用这种影响类似于排斥,但更为严厉。 |
Exclusion | 从两个图像的总和中减去两个图像乘积的两倍。合成黑没有效果;合成白色将反转其他图像的颜色。输出图像的不透明度的计算方法与SrcOver相同。注意这个BlendMode只能在androidapi级别29及以上使用效果类似于差异,但更柔和。 |
Multiply | 将源图像和目标图像的分量相乘,包括alpha通道。这只能产生相同或较深的颜色(乘以白色,1.0,结果不变;乘以黑色(0.0,结果为黑色)。由于alpha通道也会相乘,因此一个图像中的完全透明像素(不透明度0.0)会导致输出中的完全透明像素。这与DstIn类似,但颜色组合在一起。 |
Hue | 获取源图像的色调,以及目标图像的饱和度和亮度。其效果是用源图像着色目标图像。输出图像的不透明度的计算方法与SrcOver相同。在源图像中完全透明的区域从目标图像获取其色调。注意这个BlendMode只能在androidapi级别29及以上使用。 |
Saturation | 获取源图像的饱和度,以及目标图像的色调和亮度。输出图像的不透明度的计算方法与SrcOver相同。在源图像中完全透明的区域从目标图像获取其饱和度。注意这个BlendMode只能在androidapi级别29及以上使用。 |
Color | 获取源图像的色调和饱和度,以及目标图像的亮度。其效果是用源图像着色目标图像。输出图像的不透明度的计算方法与SrcOver相同。源图像中完全透明的区域从目标处获取其色调和饱和度。注意这个BlendMode只能在androidapi级别29及以上使用。 |
Luminosity | 获取源图像的亮度,以及目标图像的色调和饱和度。输出图像的不透明度的计算方法与SrcOver相同。在源图像中完全透明的区域从目标图像获取其亮度。注意这个BlendMode只能在androidapi级别29及以上使用。 |
六、DrawScope
属性 | 说明 |
size | 可以拿到可以拿画布的宽高(size.width、size.height) |
center | 可以拿到画布中心点坐标 |
drawContext | 存储了以下信息:绘制尺寸size、封装的canvas、用来旋转缩放移动的transform,而通过 canvas.nativeCanvas 就能获取具体平台的实现,即可以调用 Android 原生的 Canvas 来实现更多需求。 |
行为 | inset 将DrawScope坐标空间平移 |
translate 平移左上角,整体比例不变 | |
rotate(旋转坐标)讲的是旋转了多少角度 rotateRad(旋转坐标)讲的是旋转了多少弧度 | |
scale 缩放坐标 | |
clipRect 裁剪矩形区域,绘制在裁剪好的矩形区域内。ClipOp.Difference从当前剪辑中减去提供的矩形。 | |
clipPath 裁剪路径 | |
drawIntoCanvas 直接提供底层画布 | |
withTransform 组合转换 |
6.1 绘制基本形状
6.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 变形 |
6.1.2 画矩形 drawRect()
fun drawRect( color: Color, topLeft: Offset = Offset.Zero, //左上角偏移 size: Size = this.size.offsetSize(topLeft), alpha: Float = 1.0f, style: DrawStyle = Fill, //Fill填充、Stoke描边 colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) |
6.1.3 画圆角矩形 drawRoundRect()
fun drawRoundRect( color: Color, topLeft: Offset = Offset.Zero, size: Size = this.size.offsetSize(topLeft), cornerRadius: CornerRadius = CornerRadius.Zero, //圆角半径 style: DrawStyle = Fill, //Fill填充、Stoke描边 alpha: Float = 1.0f, colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) |
6.1.4 画圆形 drawCircle()
fun drawCircle( color: Color, radius: Float = size.minDimension / 2.0f, //半径 center: Offset = this.center, //圆心偏移 alpha: Float = 1.0f, style: DrawStyle = Fill, //Fill填充、Stoke描边 colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) |
6.1.5 画椭圆形 drawOval()
fun drawOval( color: Color, topLeft: Offset = Offset.Zero, //左上角偏移 size: Size = this.size.offsetSize(topLeft), //传入宽高 Size(200f, 100f) alpha: Float = 1.0f, style: DrawStyle = Fill, //Fill填充、Stoke描边 colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) |
6.1.6 画弧度跟扇形 drawArc()
fun drawArc( color: Color, startAngle: Float, //起始点角度 sweepAngle: Float, //扫过的角度 useCenter: Boolean, //是否连接中心(true扇形、false弧形) topLeft: Offset = Offset.Zero, //左上角偏移 size: Size = this.size.offsetSize(topLeft), //传入宽高 Size(200f, 100f) alpha: Float = 1.0f, style: DrawStyle = Fill, //Fill填充、Stoke描边(弧形填充就是半圆) colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) |
6.1.7 画点 drawPoints()
fun drawPoints( points: List<Offset>, //点坐标的集合 pointMode: PointMode, //如何绘制点 color: Color, strokeWidth: Float = Stroke.HairlineWidth, //描边宽度(线宽) cap: StrokeCap = StrokeCap.Butt, //线两头形状 pathEffect: PathEffect? = null, alpha: Float = 1.0f, colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) | |
PointMode 如何绘制点 | PointMode.Points 分别画点 |
PointMode.Lines 画线(点集合两两组合划线,奇数的话最后一个不管) | |
PointMode.Polygon 画多边形(最后一个点偏移要跟第一个一样才闭口,否则只是连接各点) |
6.2 绘制路径
6.2.1 drawPath()
fun drawPath( path: Path, color: Color, alpha: Float = 1.0f, style: DrawStyle = Fill, //Fill填充、Stoke描边(弧形填充就是半圆) colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) |
6.3 绘制图片
6.3.1 drawImage()
fun drawImage( image: ImageBitmap, //源图像 topLeft: Offset = Offset.Zero, //左上角偏移 alpha: Float = 1.0f, style: DrawStyle = Fill, //Fill填充、Stoke描边 colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) |
fun drawImage( image: ImageBitmap, srcOffset: IntOffset = IntOffset.Zero, //源图像的偏移 srcSize: IntSize = IntSize(image.width, image.height), //源图像大小 dstOffset: IntOffset = IntOffset.Zero, //绘制图像的偏移 dstSize: IntSize = srcSize, //绘制图像的大小 alpha: Float = 1.0f, style: DrawStyle = Fill, //Fill填充、Stoke描边 colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) |
6.4 绘制文本
6.5 用原生Canvas绘制
6.5.1 drawIntoCanvas()
inline fun DrawScope.drawIntoCanvas(block: (Canvas) -> Unit) |
drawIntoCanvas {
val paint = Paint()
paint.color = Color.Red
paint.strokeWidth = 10f
it.drawLine(p1= Offset(50f,50f),p2= Offset(200f,200f),paint = paint)
}
6.6 转换
6.6.1 平移 translate()、inset()
inline fun DrawScope.translate( left: Float = 0.0f, top: Float = 0.0f, block: DrawScope.() -> Unit ) { drawContext.transform.translate(left, top) block() drawContext.transform.translate(-left, -top) } |
@Composable
fun Demo2() {
Canvas(
modifier = Modifier.size(200.dp).background(Color.Blue)
) {
val halfSize = size / 2F
//正常画
drawRect(
color = Color.Green,
size = halfSize
)
//平移后画
translate(left = 20F, top = 20F) {
drawRect(
color = Color.Red,
size = halfSize
)
}
}
}
6.6.2 缩放 scale()
inline fun DrawScope.scale( |
inline fun DrawScope.scale( scaleX: Float, //横向缩放大小 scaleY: Float, //纵向缩放大小 pivot: Offset = center, //中心点偏移 block: DrawScope.() -> Unit ) |
6.6.3 旋转 rotate()、rotateRad()
inline fun DrawScope.rotate( degrees: Float, //顺时针旋转的度数 pivot: Offset = center, //中心点偏移 block: DrawScope.() -> Unit ) |
inline fun DrawScope.rotateRad( radians: Float, //顺时针旋转的弧度 pivot: Offset = center, //中心点偏移 block: DrawScope.() -> Unit ) |
6.6.4 裁剪 clipRect()、clipPath()
inline fun DrawScope.clipRect( left: Float = 0.0f, top: Float = 0.0f, right: Float = size.width, bottom: Float = size.height, clipOp: ClipOp = ClipOp.Intersect, block: DrawScope.() -> Unit ) |
inline fun DrawScope.clipPath( path: Path, clipOp: ClipOp = ClipOp.Intersect, block: DrawScope.() -> Unit ) |
6.7 组合多种转换
6.7.1 withTransform()
inline fun DrawScope.withTransform( transformBlock: DrawTransform.() -> Unit, drawBlock: DrawScope.() -> Unit ) | ||
DrawTransform 作用域中依然可以调用 size 和 center | 平移 | fun translate(left: Float = 0.0f, top: Float = 0.0f) fun inset(left: Float, top: Float, right: Float, bottom: Float) |
缩放 | fun scale(scaleX: Float, scaleY: Float, pivot: Offset = center) | |
旋转 | fun rotate(degrees: Float, pivot: Offset = center) | |
裁剪 | fun clipRect( fun clipPath(path: Path, clipOp: ClipOp = ClipOp.Intersect) |