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…