一、概念
所有的绘制操作都是通过调整像素大小来执行的。若要确保项目在不同的设备密度和屏幕尺寸上都能采用一致的尺寸,请务必使用 .toPx() 对 dp 进行转换或者采用小数尺寸。
| fun Dp.toPx(): Float = value * density |
val width = 50.dp.toPx()
1.1 混合模式 BlendMode
不指定任何模式,绘制两个图案,后面的会叠加在前面之上,通过 BlendMode 可以对重叠部分的效果进行设置。源图指的是即将绘制的图像,目标图则是已存在的图像。

1.1.1 Alpha 合成
| Src | 只显示源图,删除目标图。 |
| SrcOver(默认) | 都显示,源图绘制在目标图上。 |
| SrcIn | 只显示重叠部分,重叠部分来自源图。 |
| SrcAtop | 只显示目标图,但重叠部分来自源图。 |
| Dst | 只显示目标图,丢弃源图的绘制。 |
| DstOver | 都显示,但重叠部分来自目标图。 |
| DstIn | 只显示重叠部分,重叠部分来自目标图。 |
| DstAtop | 只显示源图,但重叠部分来自目标图。 |
| Clear | 都不显示,删除目标图,丢弃源图绘制。 |
| SrcOut | 只显示源图,但重叠部分不显示。 |
| DstOut | 只显示目标图,但重叠部分不显示。 |
| Xor | 都显示,但不显示重叠部分。 |
1.1.2 颜色混合

| Darken | 通过从每个颜色通道中选择最低值来合成源图像和目标图像。 输出图像的不透明度的计算方法与SrcOver相同。 |
| Lighten | 通过选择每个颜色通道中的最高值来合成源图像和目标图像。 输出图像的透明度计算方式与 SrcOver 模式相同。 |
| Multiply (api29+) | 将源图像和目标图像的每个分量(包括 Alpha 通道)进行相乘。 此操作只能产生相同或更暗的颜色(乘以白色 1.0 保持不变,乘以黑色 0.0 结果为黑色)。 由于 Alpha 通道也会被相乘,若任一图像中的像素完全透明(透明度 0.0),则输出图像中对应像素也将完全透明。这与 DstIn 模式相似,但区别在于此处颜色通道也参与了运算。 若需要仅对颜色通道进行乘法运算而保留 Alpha 通道不变,请考虑使用 Modulate 模式。 |
| Screen | 对源图像和目标图像的分量取反后进行相乘,再将结果取反。 这里的"分量取反"意味着:完全饱和的通道(不透明白色)被视为 0.0,而通常被视为 0.0 的值(黑色、透明)则被视为 1.0。 此模式本质上与 Modulate 混合模式相同,区别在于乘法运算前对颜色值进行取反,渲染前再将结果取反。 此操作只能产生相同或更亮的颜色(乘以黑色 1.0 保持不变,乘以白色 0.0 结果为白色)。类似地,在 Alpha 通道中,它只能使像素变得更不透明。 其效果类似于两台投影仪同时将图像投射到同一屏幕上时产生的叠加效果。 |
| Overlay | 对源图像和目标图像的分量进行乘法运算,但调整过程会倾向于保留目标图像的特征。 具体计算规则如下: 若目标值较小,则将其与源值直接相乘。 若源值较小,则先对源值和目标值取反,再将两个取反后的值相乘,最后对结果取反。 这里的"分量取反"意味着:完全饱和的通道(不透明白色)被视为 0.0,而通常被视为 0.0 的值(黑色、透明)则被视为 1.0。 |
| Plus | 将源图像和目标图像的每个分量进行相加。 任一图像中像素的透明度会降低该图像对输出像素的贡献,其效果相当于该图像中该像素的颜色变暗。 |
| Modulate |
此操作只能产生相同或更暗的颜色(乘以白色 1.0 保持不变,乘以黑色 0.0 结果为黑色)。 当合成两个不透明图像时,其效果类似于将两张幻灯片重叠在投影仪上。 若需要同时相乘 Alpha 通道,请考虑使用 Multiply 模式。 |
| ColorDodge (api29+) | 用源图像的逆分量除目标图像。 "分量取反"意味着:完全饱和的通道(不透明白色)被视为 0.0,而通常被视为 0.0 的值(黑色、透明)则被视为 1.0。 |
| ColorBurn (api29+) | 用源图像除目标图像的逆分量,再将结果取反。 "分量取反"意味着:完全饱和的通道(不透明白色)被视为 0.0,而通常被视为 0.0 的值(黑色、透明)则被视为 1.0。 |
| Hardlight (api29+) | 对源图像和目标图像的分量进行乘法运算,但调整过程会倾向于保留源图像的特征。 具体计算规则如下: 若源值较小,则将其与目标值直接相乘。 若目标值较小,则先对目标值和源值取反,再将两个取反后的值相乘,最后对结果取反。 这里的"分量取反"意味着:完全饱和的通道(不透明白色)被视为 0.0,而通常被视为 0.0 的值(黑色、透明)则被视为 1.0。 |
| Softlight (api29+) | 对于低于 0.5 的源值采用 ColorDodge 模式,对于高于 0.5 的源值采用 ColorBurn 模式。 此效果与 Overlay 相似,但结果更为柔和。 |
| Difference (api29+) | 对每个通道执行较大值减去较小值的运算。 合成黑色无效果;合成白色则会反转另一图像的颜色。 输出图像的透明度计算方式与 SrcOver 模式相同。 |
| Exclusion (api29+) | 从两图像之和中减去两图像乘积的两倍。 合成黑色无效果;合成白色则会反转另一图像的颜色。 输出图像的透明度计算方式与 SrcOver 模式相同。 |
| Hue (api29+) | 采用源图像的色相,以及目标图像的饱和度和亮度。其效果相当于使用源图像对目标图像进行着色处理。 输出图像的透明度计算方式与 SrcOver 模式相同。源图像中完全透明的区域,其色相会从目标图像中获取。 |
| Saturation (api29+) | 采用源图像的饱和度,以及目标图像的色相和亮度。 输出图像的透明度计算方式与 SrcOver 模式相同。源图像中完全透明的区域,其饱和度会从目标图像中获取。 |
| Color (api29+) | 采用源图像的色相和饱和度,以及目标图像的亮度。其效果相当于使用源图像对目标图像进行着色处理。 输出图像的透明度计算方式与 SrcOver 模式相同。源图像中完全透明的区域,其色相和饱和度会从目标图像中获取。 |
| Luminosity (api29+) | 采用源图像的亮度,以及目标图像的色相和饱和度。 输出图像的透明度计算方式与 SrcOver 模式相同。源图像中完全透明的区域,其亮度会从目标图像中获取。 |
1.2 颜色滤镜 ColorFilter
| tint() | fun tint( color: Color, blendMode: BlendMode = BlendMode.SrcIn ): ColorFilter 染色。滤镜作为源图,背景作为目标图,一起参与混合模式。 |
| colorMatrix() | fun colorMatrix(colorMatrix: ColorMatrix): ColorFilter 创建一个通过 4x5 颜色矩阵转换颜色的滤镜。此滤镜可用于调整像素饱和度、执行 YUV 到 RGB 的色彩空间转换等操作。 |
| lighting() | fun lighting(multiply: Color, add: Color): ColorFilter 创建一个可用于模拟简单光照效果滤镜。光照 ColorFilter 由两个参数定义:一个用于乘以源颜色,另一个用于加到源颜色。 |
1.3 坐标系区别

当内容进入 Popup、自定义 Layout 或 Canvas,它只是进入了一个新的节点或容器,local 原点重新归零。进入哪个节点,就以哪个节点的左上角为局部原点。所以同一个锚点中心,在 window 中可能是 x = 620px,换到 Popup local 后可能只剩 x = 180px。
root 与 window 在简单全屏页面里可能数值相近,但不能因此当成同一个概念。系统栏、Dialog、Popup、嵌入式 Compose 容器或不同平台窗口都可能让两者出现偏移。
跨容器对齐时,先把数据统一到 window,进入目标容器后,再减去目标容器的 window 原点,得到它的 local 坐标。
- local:当前节点自身。
- root:当前 Compose UI树的根布局。
- window:当前承载界面的窗口。
| 常见API | 结果相对谁 | 适合解决什么 |
| positionInParent() | 直接父布局 | 普通父子对齐 |
| positionInRoot() | UI树根布局 | 同一根布局内跨层比较 |
| positionInWindow() | 当前定位窗口 | Popup、平台窗口内定位 |
| boundsInWindow() | 当前定位窗口 | 直接获得节点窗口边界 |
二、画布 DrawScope
绘制都是在 DrawScope 中发生的,可以理解为画布,提供了绘制所需的上下文信息属性,封装了一系列的绘制API。
| 属性 | 说明 |
| size | 可以拿到可以拿画布的宽高(size.width、size.height)。 |
| center | 可以拿到画布中心点坐标。 |
| drawContext | 存储了以下信息:绘制尺寸size、封装的canvas、用来旋转缩放移动的transform,而通过 canvas.nativeCanvas 就能获取具体平台的实现,即可以调用 Android 原生的 Canvas 来实现更多需求。 |
2.1 通过 Modifier 修饰符
在已有的组件之上或之下绘制。
| .drawBehind() | fun Modifier.drawBehind( 在被修饰项的后面绘制内容(底层是先绘制 Lambda 再绘制被修饰项,后绘制的会显示在上面),适合绘制背景边框。 |
| .drawWithContent() | fun Modifier.drawWithContent( 可以掌控绘制顺序,手动调用 drawContent() 绘制被修饰项,后绘制的会显示在上面。 |
| .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)
)
}
2.2 通过 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
主要用于绘制渐变色,需要纯色填充使用 SolidColor。很多 API 都提供了 Color 或 Brush 两套重载,能用 Color 表达清楚时不需要强行换成 Brush,不要把 Brush 当成更高级的 Color,Color 解决“是什么颜色”,Brush 解决“区域如何被填充”。一般先用 Color 建立清晰层级,只有当单色表达不够时再引入 Brush。
| Color 更适合 | 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 |
更适合圆形进度、仪表盘、环形图这类围绕中心绘制的组件。 |
| SolidColor 纯色填充 | class SolidColor( val value: Color ) : Brush(), Interpolatable | val solidBrush = SolidColor(Color(0xFF2563EB)) 在只接收 Brush 的地方,SolidColor 就有用。 |
| 网格渐变 | 见下方七章。 |
Compose v1.12 引入,创建多个点实现自然的颜色渐变。 |
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))
六、绘制
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 路径 drawPath()
对于不规则形状,点会非常多需要频繁调用画线方法,画路径方法只需要调用一次。
| fun drawPath( path: Path, color: Color, alpha: Float = 1.0f, style: DrawStyle = Fill, //Fill填充、Stoke描边(弧形填充就是半圆) colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) |
// 创建路径(围绕组件一圈)
val path = Path().apply {
moveTo(0F, 0F) // 左上
lineTo(size.width, 0F) // 右上
lineTo(size.width, size.height) // 右下
lineTo(0F, size.height) //左下
close()
}
6.3 图片 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 文本 drawText()
| drawText() | fun DrawScope.drawText( textMeasurer: TextMeasurer, text: AnnotatedString, topLeft: Offset = Offset.Zero, style: TextStyle = TextStyle.Default, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(), size: Size = Size.Unspecified, blendMode: BlendMode = DrawScope.DefaultBlendMode, ) |
| fun DrawScope.drawText( textMeasurer: TextMeasurer, text: String, topLeft: Offset = Offset.Zero, style: TextStyle = TextStyle.Default, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, size: Size = Size.Unspecified, blendMode: BlendMode = DrawScope.DefaultBlendMode, ) |
val textMeasurer = rememberTextMeasurer()
Canvas(modifier = Modifier.fillMaxSize()) {
drawText(textMeasurer, "Hello")
}
6.5 用Canvas对象绘制 drawIntoCanvas()
可以将已有的 View 项目中的绘制逻辑搬过来用。
| inline fun DrawScope.drawIntoCanvas(block: (Canvas) -> Unit) |
drawIntoCanvas { canvas ->
val paint = Paint() //注意这里画笔的导包不要是Compose的
paint.color = Color.Red
paint.strokeWidth = 10f
canvas.drawLine(p1= Offset(50f,50f),p2= Offset(200f,200f),paint = paint)
}
6.6 画布操作
这些操作函数会先保存当前画布状态→执行 Lambda→然后恢复,这比 View 中的 canvas.save() / canvas.restore() 更安全不会导致状态栈溢出。
6.6.1 平移 translate()、inset()
| translate() | inline fun DrawScope.translate( 平移左上角,整体比例不变。 |
| inset() | inline fun DrawScope.inset( inset: Float, block: DrawScope.() -> Unit ) |
| inline fun DrawScope.inset( horizontal: Float = 0.0f, vertical: Float = 0.0f, block: DrawScope.() -> Unit, ) | |
| inline fun DrawScope.inset( left: Float, top: Float, right: Float, bottom: Float, block: DrawScope.() -> Unit, ) |

@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()
| scale() | inline fun DrawScope.scale( |
| inline fun DrawScope.scale( scaleX: Float, //横向缩放大小 scaleY: Float, //纵向缩放大小 pivot: Offset = center, //中心点偏移 block: DrawScope.() -> Unit ) |
6.6.3 旋转 rotate()、rotateRad()
| rotate() | inline fun DrawScope.rotate( 旋转了多少角度。 |
| rotateRad() | inline fun DrawScope.rotateRad( 旋转了多少弧度。 |
6.6.4 裁剪 clipRect()、clipPath()
| clipRect() | inline fun DrawScope.clipRect( 裁剪矩形区域,绘制在裁剪好的矩形区域内。ClipOp.Difference从当前剪辑中减去提供的矩形。 |
| clipPath() | inline fun DrawScope.clipPath( 裁剪路径。 |
6.6.5 组合多种操作 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) | |
七、网格渐变 MeshGradientPainter
| 网格维度 | 网格沿垂直轴和水平轴拆分为多个块。rows 和 columns 的网格包含 (rows+1)×(columns+1) 个顶点。例如,1x1 网格由 4 个顶点组成,形成一个面片。 |
| 归一化坐标 | 所有顶点位置都使用归一化坐标系,其中 (0f, 0f) 表示绘制边界的左上角,(1f, 1f) 表示绘制边界的右下角。 |
| Bezier控制点(切线) | 每个顶点最多包含四个可选的 Bezier 控制点。这些正切指定了相邻顶点之间的边曲率。如果您使用 Offset.Unspecified,Compose 会推断出切线,以确保补丁之间的平滑过渡。由 4 个顶点及其控制点形成的每个网格单元都会生成一个贝塞尔曲面片。 |
| 颜色插值 | 框架会计算主要顶点之间的颜色。将 hasBicubicColor 设置为 true 可实现 Catmull-Rom 插值,从而实现更平滑的颜色过渡;设置为 false 可实现双线性插值。 |
7.1 简单实现

指定其维度,并在配置块内使用 setVertex 函数来定位角点并为其分配颜色。
val rows = 1
val columns = 1
val gradientPainter = remember {
MeshGradientPainter(rows, columns) {
// Parameters: row, column, position, color
setVertex(0, 0, Offset(0f, 0f), Color.Red) // 左上
setVertex(0, 1, Offset(1f, 0f), Color.Blue) // 右上
setVertex(1, 0, Offset(0f, 1f), Color.Green) // 左下
setVertex(1, 1, Offset(1f, 1f), Color.Yellow) // 右下
}
}
Box(
modifier = modifier
.aspectRatio(16/9f)
.fillMaxWidth()
.paint(gradientPainter)
)
7.2 使用贝塞尔控制点

默认情况下,网格生成器会处理复杂的计算,以保持网格过渡平滑。不过,如果您想有选择地推拉或急剧收缩某些颜色部分,可以明确自定义任何单个顶点的切线。控制偏移量是相对于宿主顶点的位置来衡量的。如需完全停用切线推理,并改用完全笔直的线性边界,请为顶点控制点分配 Offset.Zero 的偏移量。
val customTangentPainter = remember {
MeshGradientPainter(rows = 1, columns = 1) {
// 调整左上角顶点,使其向右和向下向外弯曲
setVertex(
row = 0,
column = 0,
position = Offset(0f, 0f),
color = Color.Magenta,
rightControlPoint = Offset(0.4f, 0.1f),
bottomControlPoint = Offset(0.1f, 0.4f)
)
// 其他点可以保持未指定,以使用默认推断的备用切线
setVertex(0, 1, Offset(1f, 0f), Color.Cyan)
setVertex(1, 0, Offset(0f, 1f), Color.Blue)
setVertex(1, 1, Offset(1f, 1f), Color.Black)
}
}
Box(
modifier = modifier
.aspectRatio(16/9f)
.fillMaxWidth()
.paint(customTangentPainter)
)
7.3 高级实现

3x3 网格,这意味着需要指定 16 个点,并使用不同的偏移量设置中间点。
val points = remember {
listOf(
Offset(0.0f, 0.0f), Offset(0.3f, 0.0f), Offset(0.7f, 0.0f), Offset(1.0f, 0.0f),
Offset(0.0f, 0.3f), Offset(0.2f, 0.4f), Offset(0.7f, 0.2f), Offset(1.0f, 0.3f),
Offset(0.0f, 0.7f), Offset(0.3f, 0.8f), Offset(0.7f, 0.6f), Offset(1.0f, 0.7f),
Offset(0.0f, 1.0f), Offset(0.3f, 1.0f), Offset(0.7f, 1.0f), Offset(1.0f, 1.0f)
)
}
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
// Row 0
setVertex(0, 0, points[0], yellow)
setVertex(0, 1, points[1], orange)
setVertex(0, 2, points[2], yellow)
setVertex(0, 3, points[3], purple)
// Row 1
setVertex(1, 0, points[4], pink)
setVertex(1, 1, points[5], yellow)
setVertex(1, 2, points[6], pink)
setVertex(1, 3, points[7], purple)
// Row 2
setVertex(2, 0, points[8], indigo)
setVertex(2, 1, points[9], pink)
setVertex(2, 2, points[10], purple)
setVertex(2, 3, points[11], indigo)
// Row 3
setVertex(3, 0, points[12], purple)
setVertex(3, 1, points[13], indigo)
setVertex(3, 2, points[14], pink)
setVertex(3, 3, points[15], yellow)
}
}
Box(
modifier = modifier.padding(32.dp)
.aspectRatio(16 / 9f)
.fillMaxWidth()
.paint(gradientPainter)
// ...
)
7.4 以动画方式呈现

由于 MeshGradientPainter 的 block lambda 参数是在 DrawScope 中执行的,因此它可以读取和观察可变状态。您可以随时间推移为位置或颜色添加动画效果,而无需重新分配着色器或位图。
val infiniteTransition = rememberInfiniteTransition(label = "meshMovement")
val animatedOffset by infiniteTransition.animateFloat(
initialValue = -0.1f,
targetValue = 0.1f,
animationSpec = infiniteRepeatable(
animation = tween(2500, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "offset"
)
val coral = Color(255, 90, 90)
val peach = Color(255, 139, 90)
val amber = Color(255, 169, 90)
val sunshine = Color(255, 212, 90)
val indigo = Color(0xFF5856D6)
val pink = Color(0xFFFF2D55)
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
// Row 0
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
setVertex(0, 1, Offset(0.3f, 0.0f), peach)
setVertex(0, 2, Offset(0.7f, 0.0f), amber)
setVertex(0, 3, Offset(1.0f, 0.0f), sunshine)
// Row 1
setVertex(1, 0, Offset(0.0f, 0.3f), pink)
setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
setVertex(1, 2, Offset(0.7f, 0.2f) + Offset(animatedOffset, animatedOffset), peach)
setVertex(1, 3, Offset(1.0f, 0.3f), indigo)
// Row 2
setVertex(2, 0, Offset(0.0f, 0.7f), coral)
setVertex(2, 1, Offset(0.3f, 0.8f) + Offset(animatedOffset, 0f), pink)
setVertex(2, 2, Offset(0.7f, 0.6f) + Offset(animatedOffset, 0f), sunshine)
setVertex(2, 3, Offset(1.0f, 0.7f), amber)
// Row 3
setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
setVertex(3, 1, Offset(0.3f, 1.0f), amber)
setVertex(3, 2, Offset(0.7f, 1.0f), pink)
setVertex(3, 3, Offset(1.0f, 1.0f), indigo)
}
}
Box(
modifier = modifier.padding(32.dp)
.safeContentPadding()
.aspectRatio(16 / 9f)
.fillMaxWidth()
.paint(gradientPainter)
)











2704



被折叠的 条评论
为什么被折叠?



