Canvas很强大,能绘制的东西很多,我们前面学到了使用Canvas绘制点、线和矩形等,今天我们看看Canvas的其它绘制。
Canvas绘制圆和椭圆
圆在平时开发中很常见,比如绘制饼状图或指示器圆点等。同绘制点、线一样,绘制圆的方法也是在DrawScope中定义,如下所示:
fun drawCircle( color: Color, // 颜色 radius: Float = size.minDimension / 2.0f, // 半径 center: Offset = this.center, // 圆心坐标 /*@FloatRange(from = 0.0, to = 1.0)*/ alpha: Float = 1.0f, // 透明度 style: DrawStyle = Fill, // 样式,默认是填充 colorFilter: ColorFilter? = null, // 颜色效果 blendMode: BlendMode = DefaultBlendMode // 混合模式 )
绘制一个圆重点就是圆心坐标和半径。上面这些参数中,color是必须的,半径有默认值,是当前Canvas画布的宽或高较小值的一半;center就是圆心坐标,默认值是Canvas的中心点;alpha是透明度,style用来设置圆的样式;colorFilter用来设置圆的效果;blendMode用来设置混合模式。都了解了这些参数后,试着去绘制一个圆:
@Composable fun DrawCircleTest() { Canvas(modifier = Modifier.size(360.dp)) { drawCircle( color = Color.Blue, radius = 300f, center = center ) } }
如上,我们绘制了一个半径为300f的圆,圆心坐标为画布的中心点,效果如下:

可以看到我们成功地绘制了一个圆。如果想要空心圆,通过设置其style参数即可:
@Composable fun DrawCircleTest() { Canvas(modifier = Modifier.size(360.dp)) { drawCircle( color = Color.Blue, radius = 300f, center = center, style = Stroke( width = 30f ) ) } }
上面代码我们只是将style由Fill修改为Stroke,并设置宽度为30f:

绘制椭圆
一起看下绘制椭圆的方法:
fun drawOval( color: Color, topLeft: Offset = Offset.Zero, size: Size = this.size.offsetSize(topLeft), /*@FloatRange(from = 0.0, to = 1.0)*/ alpha: Float = 1.0f, style: DrawStyle = Fill, colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode )
参数和drawRect的参数一模一样,是不是很奇怪?先来绘制一个椭圆:
@Composable fun DrawOvalTest() { val topLeft = Offset(100f, 100f) val ovalSize = Size(600f, 800f) Canvas(modifier = Modifier.size(360.dp)) { drawOval( color = Color.Blue, topLeft = topLeft, size = ovalSize ) } }

既然绘制椭圆和矩形的参数一样,那么我们使用相同的参数去绘制矩形和椭圆,会怎么样呢?来试试:
val topLeft = Offset(100f, 100f) val ovalSize = Size(600f, 800f) Canvas(modifier = Modifier.size(360.dp)) { drawOval( color = Color.Blue, topLeft = topLeft, size = ovalSize ) drawRect( color = Color.Red, topLeft = topLeft, size = ovalSize ) }

这就很明确了,椭圆其实就是对矩形做内切形成的。
绘制圆弧、图片和路径
- 绘制圆弧 同绘制点、线等方法一样,绘制圆弧的方法也是在DrawScope中定义的:
fun drawArc( color: Color, // 颜色 startAngle: Float, // 起始角度,0代表3点钟方向 sweepAngle: Float, // 相对于startAngle顺时针绘制的弧度(单位:度) useCenter: Boolean, // 设置圆弧是否要关闭边界中心的标志 topLeft: Offset = Offset.Zero, // 左上角坐标点 size: Size = this.size.offsetSize(topLeft), // 大小 /*@FloatRange(from = 0.0, to = 1.0)*/ alpha: Float = 1.0f, // 透明度 style: DrawStyle = Fill, // 样式,Fill 或 Stroke colorFilter: ColorFilter? = null, // 颜色效果 blendMode: BlendMode = DefaultBlendMode // 混合模式 )
来看个例子:
@Composable fun DrawArcTest() { Canvas(modifier = Modifier.size(360.dp)) { drawArc( color = Color.Blue, startAngle = 0f, sweepAngle = 90f, useCenter = true ) } }

上面的例子我们关闭了边界中心的表示useCenter = true;我们修改下代码:
@Composable fun DrawArcTest() { Canvas(modifier = Modifier.size(360.dp)) { drawArc( color = Color.Blue, startAngle = 90f, sweepAngle = 150f, useCenter = false ) } }

可以看到,如果useCenter设置为true,圆弧会连接中心点,反之不会。如果想要绘制空心圆弧,同样设置其style即可:
Canvas(modifier = Modifier.size(360.dp)) { drawArc( color = Color.Blue, startAngle = 90f, sweepAngle = 150f, useCenter = false, style = Stroke(width = 10f) ) }

- 绘制图片 绘制图片方法同样在DrawScope中:
fun drawImage( image: ImageBitmap, // 图片资源 srcOffset: IntOffset = IntOffset.Zero, // 可选偏移量,代表要绘制的原图片的左上偏移量 srcSize: IntSize = IntSize(image.width, image.height), // 相对于srcOffset绘制的原图片可选尺寸,默认为image的宽度和高度 dstOffset: IntOffset = IntOffset.Zero, // 可选偏移量,表示绘制给定图片的目标位置的左上偏移量 dstSize: IntSize = srcSize, // 要绘制的目标图片的可选尺寸,默认为srcSize /*@FloatRange(from = 0.0, to = 1.0)*/ alpha: Float = 1.0f, // 透明度 style: DrawStyle = Fill, // 样式 colorFilter: ColorFilter? = null, // 颜色效果 blendMode: BlendMode = DefaultBlendMode // 混合模式 )
可以看到只有资源是必填参数,其他都是由默认值可选参数,一起绘制一张图片:
@Composable fun DrawImageTest() { val context = LocalContext.current val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.small) val image = bitmap.asImageBitmap() Canvas(modifier = Modifier.size(360.dp)) { drawImage( image = image ) } }
通过BitmapFactory获取图片资源,再通过asImageBitmap()方法转成方法需要的ImageBitmap,最后绘制上去:

参数srcOffset,它类型是IntOffset, 是可选偏移量,代表要绘制的原图片左上偏移量。之前没见过IntOffset,但是见过Offset,Offset设置的时候参数类型是Float。IntOffset的使用方法如下:
@Stable fun IntOffset(x: Int, y: Int): IntOffset = IntOffset(packInts(x, y))
参数也是x、y。srcSize类型为IntSize,它的作用是相对于srcOffset绘制的源图片的可选尺寸。IntSize使用方法和Size一样,也是传入宽和高,只不过参数类型由Float变为了Int。
再看看dstOffset,类型是IntOffset,也是可选偏移量,表示绘制给定图片的目标位置的左上偏移量,默认为当前的原点,以绘制目标图片的目标位置的左上偏移量作为默认值。
最后看看参数dstSize,类型是IntSize,是绘制的目标图片的可选尺寸,默认是srcSize。设置dstSize就可以设置绘制图片的尺寸。看个例子:
@Composable fun DrawImageTest() { val context = LocalContext.current val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.small) val image = bitmap.asImageBitmap() Canvas(modifier = Modifier.size(360.dp)) { drawImage( image = image, srcOffset = IntOffset(0, 0), srcSize = IntSize(100, 100), dstOffset = IntOffset(100, 100), dstSize = IntSize(800, 800) ) } }
我们将srcOffset设置为左上角,没有偏移;将srcSize宽高都设置为100;将dstOffset宽高设置为偏移100;将dstSize宽高设置为800。效果如下:

在实际开发中绘制图片的时候要牢记:srcOffset和srcSize是用来设置源图片的,dstOffset和dstSize才是用来设置目标图片的。
- 绘制路径 Path类将多种复合路径(如之前学习的点、线段、贝塞尔曲线)等封装在其内部,即:使用Path就可以来绘制前面所绘制的所有图形。来看看Path的定义:
fun drawPath( path: Path, color: Color, /*@FloatRange(from = 0.0, to = 1.0)*/ alpha: Float = 1.0f, style: DrawStyle = Fill, colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode )
可以看到,只有第一个参数path之前没见过,其他的我们都已经了解了。参数path类型是Path,这里的Path并不是Android View中的Path,Compose中重写了Path,不过为我们提供了用户相互转换的扩展函数;Android View中的Path可以通过Path.asComposePath方法转为Compose中的Path,Compose中Path可以通过Path.asAndroidPath方法转换为Android View中的Path。
由于有了扩展方法,在使用drawPath的时候如果对Compose中的Path不熟悉,就可以使用Android View中的Path,然后通过扩展方法进行转换,不过不推荐这种做法,因为Compose中的Path已经实现了Android View中的Path功能。来看看Compose中的Path源码:
expect fun Path(): Path /* expect class */ interface Path { /** * 绘制路径内部的填充方式 */ var fillType: PathFillType /** * 返回路径的凸度,由路径的内容定义 */ val isConvex: Boolean /** * 如果路径为空(不包含直线或曲线),则返回true */ val isEmpty: Boolean /** * 在给定坐标点处开始一个新的子路径 */ fun moveTo(x: Float, y: Float) /** * 从当前点以给定偏移量开始一个新的子路径 */ fun relativeMoveTo(dx: Float, dy: Float) /** * 从当前点到给定点添加一条直线段 */ fun lineTo(x: Float, y: Float) /** * 从当前点到当前点相聚给定偏移量的点添加一条直线段 */ fun relativeLineTo(dx: Float, dy: Float) /** * 使用控制点(x1、y1)添加从当前点到给定点(x2、y2)弯曲的二阶贝塞尔曲线段 */ fun quadraticBezierTo(x1: Float, y1: Float, x2: Float, y2: Float) /** * 使用从当前点偏移(dx1、dy1)的控制点 * 添加一个从当前点弯曲到与当前点偏移(dx2、dy2)的点的二阶贝塞尔曲线段 */ fun relativeQuadraticBezierTo(dx1: Float, dy1: Float, dx2: Float, dy2: Float) /** * 使用控制点(x1、y1)和(x2、y2) * 添加从当前点到给定点(x3、y3)弯曲的三阶贝塞尔曲线段 */ fun cubicTo(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float) /** * 添加一个三阶贝塞尔曲线段,曲线从当前点偏移到(dx3、dy3)处的点 * 使用的偏移量为(dx1、dy1)和(dx2、dy2)处的控制点 */ fun relativeCubicTo(dx1: Float, dy1: Float, dx2: Float, dy2: Float, dx3: Float, dy3: Float) /** * 如果参数forceMoveTo参数为false,则添加直线段和弧段 * 如果参数forceMoveTo参数为true,则启动一个新的由弧段组成的子路径 */ fun arcToRad( rect: Rect, startAngleRadians: Float, sweepAngleRadians: Float, forceMoveTo: Boolean ) { arcTo(rect, degrees(startAngleRadians), degrees(sweepAngleRadians), forceMoveTo) } /** * 如果参数forceMoveTo参数为false,则添加直线段和弧段 * 如果参数forceMoveTo参数为true,则启动一个新的由弧段组成的子路径 */ fun arcTo( rect: Rect, startAngleDegrees: Float, sweepAngleDegrees: Float, forceMoveTo: Boolean ) /** * 添加一个新的子路径,该子路径由概述给定矩形的4行组成 */ fun addRect(rect: Rect) /** * 添加一个新的子路径,该子路径由一条曲线组成,该曲线形成填充给定矩形的椭圆 */ fun addOval(oval: Rect) /** * 添加一个新的子路径,该子路径具有一个弧段 */ fun addArcRad(oval: Rect, startAngleRadians: Float, sweepAngleRadians: Float) /** * 添加一个子路径,该子路径具有一个弧段,该弧段由遵循给定矩形所界定的椭圆边缘 * 的弧组成 */ fun addArc(oval: Rect, startAngleDegrees: Float, sweepAngleDegrees: Float) /** * 添加一个圆角矩形 */ fun addRoundRect(roundRect: RoundRect) /** * 添加一个新的子路径,该子路径包含给定的“路径”偏移量和给定的“偏移量” */ fun addPath(path: Path, offset: Offset = Offset.Zero) /** * 关闭最后一个子路径,就像从子路径的当前点到第一个点花了一条直线一样 */ fun close() /** * 清除所有子路径的Path对象,使其返回到创建时的初始状态 */ fun reset() /** * 按给定的偏移量转换每个子路径的所有段 */ fun translate(offset: Offset) /** * 计算路径控制点的边界,并将结果写入边界 */ fun getBounds(): Rect /** * 将路径设置为两个指定路径进行Op操作的结果 */ fun op( path1: Path, path2: Path, operation: PathOperation ): Boolean companion object { /** * 根据给定“操作”指定的方式组合两条路径 */ fun combine( operation: PathOperation, path1: Path, path2: Path ): Path { // 省略... } } }
从上面代码可知Path是一个接口,不过我们可以通过Path的方式进行实例化:
expect fun Path(): Path
熟悉Android中的Path应该对这里面的方法都比较熟悉,很多方法连名字都一样,上面代码中都加了对应的注释,可以根据需求去选择性的使用。我们还是老样子,来看个例子:
@Composable fun DrawPathTest() { val path = Path() path.moveTo(100f, 300f) path.lineTo(100f, 700f) path.lineTo(800f, 700f) path.lineTo(900f, 300f) path.lineTo(600f, 100f) path.close() Canvas(modifier = Modifier.size(360.dp)) { drawPath( path = path, color = Color.Red, style = Stroke(width = 10f) ) } }

如上图,我们定义了Path,然后移动到一个点,之后通过lineTo方法进行连线,最后close进行闭合。
贝塞尔曲线一直是使用Path时的难点和重点,在Android View中也是,特别是自定义动画的时候也比较重要。我们一起看看绘制贝塞尔曲线的案例:
val path = Path() path.moveTo(100f, 300f) path.lineTo(100f, 700f) // 二阶贝塞尔曲线 path.quadraticBezierTo(800f, 700f, 600f, 100f) // 三阶贝塞尔曲线 path.cubicTo(700f, 200f, 800f, 400f, 100f, 100f) path.close() Canvas(modifier = Modifier.size(360.dp)) { drawPath( path = path, color = Color.Red, style = Stroke(width = 10f) ) }
我们可以看到,二阶贝塞尔曲线使用控制点(x1、y1)添加从当前点到给定点(x2、y2)进行弯曲;三阶贝塞尔曲线就是使用控制点(x1、y1)和(x2、y2),添加从当前点到给定点(x3、y3)进行弯曲,如下图:

path还有很多方法,大家可以自己去试着学习下,看看效果。
混合模式
Android View中也有混合模式,我们先看张图:

可以看到,一个圆形和一个方形通过不同的混合模式产生不同的组合效果。
在Compose中的混合模式,就是上面我们总能看到的BlendMode。BlendMode的源码如下:
@Suppress("INLINE_CLASS_DEPRECATED", "EXPERIMENTAL_FEATURE_WARNING") @Immutable inline class BlendMode internal constructor(@Suppress("unused") private val value: Int) { companion object { /** * 删除源图片和目标图片 */ val Clear = BlendMode(0) /** * 放置目标图片,仅绘制源图片 */ val Src = BlendMode(1) /** * 放置源图片,仅绘制目标图片 */ val Dst = BlendMode(2) /** * 将源图片合成到目标图片上 */ val SrcOver = BlendMode(3) /** * 将源图片合成到目标图片下 */ val DstOver = BlendMode(4) /** * 显示源图片,但仅显示两张图重叠的位置 */ val SrcIn = BlendMode(5) /** * 显示目标图片,但仅显示两张图片重叠的位置 */ val DstIn = BlendMode(6) /** * 显示源图片,但仅显示两张图片不重叠的位置 */ val SrcOut = BlendMode(7) /** * 显示目标图片,但仅显示两张图片不重叠的位置 */ val DstOut = BlendMode(8) /** * 将源图片合成到目标图片上,但仅在与目标图片重叠的位置合成 */ val SrcAtop = BlendMode(9) /** * 将目标图片合成到源图片上,但仅在与源图片重叠的位置合成 */ val DstAtop = BlendMode(10) /** * 对源图片和目标图片应用按位异或运算符,这将使它们重叠的地方保持透明 */ val Xor = BlendMode(11) /** * 将源图片和目标图片的组成部分求和 */ val Plus = BlendMode(12) /** * 将源图片和目标图片的颜色分量相乘 */ val Modulate = BlendMode(13) /** * 将源图片和目标图片的分量的逆值相乘,然后将结果相逆 */ val Screen = BlendMode(14) // The last coeff mode. /** * 调整源图片和目标图片的分量以使其适合目标,然后将它们相乘 */ val Overlay = BlendMode(15) /** * 通过从每个颜色通道中选择最小值来合成源图片和目标图片 */ val Darken = BlendMode(16) /** * 通过从每个颜色通道中选择最大值来合成源图片和目标图片 */ val Lighten = BlendMode(17) /** * 将目标除以源的倒数 */ val ColorDodge = BlendMode(18) /** * 将目标的倒数除以源,然后将结果求倒数 */ val ColorBurn = BlendMode(19) /** * 调整源图片和目标图片的分量以使其适合源图片,然后将它们相乘 */ val Hardlight = BlendMode(20) /** * 对于小于0.5的源值使用ColorDodge,对于大于0.5的源值使用ColorBurn */ val Softlight = BlendMode(21) /** * 从每个通道的较大值中减去较小值 */ val Difference = BlendMode(22) /** * 从两张图片的总和中减去两张图片乘积的两倍 */ val Exclusion = BlendMode(23) /** * 将源图片和目标图片的分量(包括Alpha通道)相乘 */ val Multiply = BlendMode(24) // The last separable mode. /** * 获取源图片的色相以及目标图片的饱和度和光度 */ val Hue = BlendMode(25) /** * 获取源图片的饱和度以及目标图片的色相和亮度 */ val Saturation = BlendMode(26) /** * 获取源图片的色相和饱和度以及目标图片的光度 */ val Color = BlendMode(27) /** * 获取源图片的亮度以及目标图片的色相和饱和度 */ val Luminosity = BlendMode(28) } // 省略...
Compose混合模式中的类型比Android View中多了11种,我们看看怎么使用混合模式:
@Composable fun DrawBlendModeTest() { Canvas(modifier = Modifier.size(360.dp)) { drawCircle( color = Color.Yellow, radius = 175f, center = Offset(350f, 350f), blendMode = BlendMode.Clear ) drawRect( color = Color.Blue, topLeft = Offset(300f, 300f), size = Size(350f, 350f), blendMode = BlendMode.Clear ) } }
我们绘制了一个圆和一个矩形,圆代表目标图片Dst,矩形代表源图片Src,然后将混合模式都设置为BlendMode.Clear。即删除源图片和目标图片,看下效果:

我们代码中为图形添加了背景色,所以使用clear的混合模式后会将其清除显示透明,所以就会显示黑色。
Compose中使用混合模式时一定要注意目标图片和源图片的区别。使用好混合模式可以做出非常炫酷的效果,可以再试试别的混合模式。
代码已上传github: github.com/Licarey/com…
评论(0)