公众号「稀有猿诉」 原文链接 降Compose十八掌之『利涉大川』| Canvas
任何一个GUI框架都会提供大量的预定义的UI部件,让开发者构建UI页面,但有些时候预定义的部件无法满足需求,这时就需要定制,甚至是自定义绘制的内容。对于Android开发者来说,这已经是家常便饭了,因为肯定有过用自定义View来实现一些特殊设计需求的经验。在Jetpack Compose中也有同样的方法来实现自定义绘制内容,今天就来学习一下。
使用Canvas来自定义内容
在Compose中, 我们用Canvas函数来绘制自定义内容,可以把它理解成为自定义View,但,它是一个函数,把绘制指令传给它就可以了:
val textMeasurer = rememberTextMeasurer()
Canvas(modifier = Modifier.fillMaxSize()) {
drawRect(Color.LightGray)
drawText(
textMeasurer = textMeasurer,
text = "降Compose十八掌",
topLeft = Offset(size.width / 4f, size.height / 2.2f)
)
drawCircle(
color = Color.Magenta,
radius = size.width / 10f,
center = Offset(size.width / 1.8f, size.height / 3f)
)
drawCircle(
color = Color.Yellow,
radius = size.width / 12f,
center = Offset(size.width / 1.6f, size.height / 4.5f)
)
drawCircle(
color = Color.Green,
radius = size.width / 14f,
center = Offset(size.width / 1.46f, size.height / 7f)
)
}
坐标系统
坐标系统,与常见的GUI坐标系统,以及View的坐标系统都是一样的,左上角是原点(0,0),x轴向右,y轴向下。
绘图上下文DrawScope
仔细看Canvas函数,可以发现,写绘制指令的地方是一个尾部lambda,这是Compose中非常常见的一种设计方式。这个lambda被定义为DrawScope对象的一个扩展函数,所以在这个lambda中可以隐式的访问DrawScope对象。我们所使用的绘制指令,以及很多参数其实都是在通过this指针隐式的调用DrawScope。对于扩展函数不熟悉的同学可以去复习一下Kotlin中函数的一些高级用法。
通过AndroidStudio的提示,也能看到隐式的this指针是一个DrawScope对象。
所以呢,当查找API文档时记得要去找DrawScope,而不是Canvas函数。其实Canvas就是一个封装的函数,也没啥东西。但还有一个略微底层一些的作为Graphics接口的对象Canvas,它与Android SDK中的Canvas对象是差不多的概念。
接下来我们重点看看如何使用绘制指令绘制出我们需要的内容。
画图形
图形(Shape)是最为常见的一类绘制目标,比如圆,椭圆,矩形,线,扇形等等。不难,看一眼就会用:
Canvas(modifier = Modifier.fillMaxSize()) {
drawRect(Color.LightGray)
drawOval(
color = Color.Green,
topLeft = Offset(50f, 50f),
size= Size(size.width / 10f, size.height / 12f)
)
drawLine(
color = Color.Yellow,
start = Offset(50 + size.width / 20f, 50f + size.height / 24f),
end = Offset(size.width / 1.8f, size.height / 3f),
strokeWidth = Stroke.DefaultMiter
)
drawCircle(
color = Color.Magenta,
radius = size.width / 5f,
center = Offset(size.width / 1.8f, size.height / 3f)
)
drawPoints(
color = Color.DarkGray,
pointMode = PointMode.Points,
strokeWidth = 50f,
points = genPoints(size.width / 2f, size.height / 3f)
)
}
画路径
路径(Path)是把一系列的数学指令转化为绘制命令,可以更为灵活的画一些曲线和图形,比如说画一个三角函数曲线:
@Composable
fun PathDemo() {
val tm = rememberTextMeasurer()
Canvas(modifier = Modifier.fillMaxSize()) {
drawRect(Color.LightGray)
drawText(tm, "cosine of [-PI, PI]", Offset(size.width / 3f, 60f))
drawLine(
color = Color.DarkGray,
start = Offset(0f, size.height / 2f),
end = Offset(size.width, size.height / 2f),
strokeWidth = 3f
)
drawLine(
color = Color.DarkGray,
start = Offset(size.width / 2f, size.height / 3f),
end = Offset(size.width / 2f, size.height * 2/ 3f),
strokeWidth = 3f
)
drawPath(genPath(size.width, size.height), Color.Magenta, style = Stroke(width = 10f))
}
}
fun genPath(width: Float, height: Float): Path {
val slices = 60
val path = Path();
path.moveTo(0f, height / 3f)
for (i in 1..slices) {
val x0 = 2f * i.toFloat() * PI.toFloat() / slices.toFloat() - PI.toFloat()
val y0 = cos(x0) * height / 6f
val x = i.toFloat() / slices.toFloat() * width
val y = y0 + height / 2f
path.lineTo(x, y)
path.moveTo(x, y)
}
path.close()
return path;
}
路径在绘制中是非常强大的功能,可以实现非常炫酷的动画效果。
画文字
文字是特别重要的UI元素,通常情况下我们都是过Text来展示文字,再与其他部件进行组合就能满足需求。一般来说不需要在自定义内容也使用文字,因为文字绘制一般来说比较复杂,因为像基线对齐,字体样式,字体大小等等,都需要考虑。文字部件Text内容其实也是用与自定义一样的更低层的API来实现的,但它把像对齐,样式,富文本等等都封装好了。
DrawScope也提供了绘制文字的函数,不过呢使用起来比较麻烦,需要详细计算文字所占用的区域大小,而文字的measure通常是非常麻烦的,因为像文字的字体以及文字大小都会影响到measure,因此measure要保存成为一个状态,这样当有影响到文字绘制的因素发生变化时,measure就会发生变化,进而触发Re-Composition:
@Composable
fun TextDemo() {
val textMeasure = rememberTextMeasurer()
Canvas(modifier = Modifier.fillMaxSize()) {
val measuredText =
textMeasure.measure(
AnnotatedString(
text =
"""
“降龙十八掌可说是【武学中的巅峰绝诣】,当真是无坚不摧、无固不破。虽招数有限,但每一招均具绝大威力。
北宋年间,丐帮帮主萧峰以此邀斗天下英雄,极少有人能挡得他三招两式,气盖当世,群豪束手。
当时共有“降龙廿八掌”,后经萧峰及他义弟虚竹子删繁就简,取精用宏,改为降龙十八掌,掌力更厚。
这掌法传到洪七公手上,在华山绝顶与王重阳、黄药师等人论剑时施展出来,王重阳等尽皆称道。”
""".trimIndent(),
spanStyle = SpanStyle(
fontSize = 20.sp,
fontWeight = FontWeight.ExtraBold,
brush = Brush.verticalGradient(listOf(Color.Magenta, Color.Cyan, Color.Blue))
)
),
constraints = Constraints.fixed(
width = (size.width / 1.6f).toInt(),
height = (size.height / 2f).toInt()
),
overflow = TextOverflow.Ellipsis,
style = TextStyle(fontSize = 18.sp)
)
drawText(
textLayoutResult = measuredText,
topLeft = Offset(60f, 60f)
)
}
}
画图片
图片(Image)是与文字类似的非常重要的UI元素,像图标,头像,表情,背景图,Banner图,以及内容中的图像都属于图片元素,一般情况下用Image函数可以用来展示图片。
对于自定义绘制内容也可以使用图片,DrawScope中有提供绘制图片的方法:
val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)
Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
drawImage(dogImage)
})
变幻
除了绘制以外,DrawScope还提供了一系列做变幻的函数。包括缩放,位移,旋转这些变幻直接作用于绘制指令上面。
缩放
使用DrawScope.scale函数来对绘制指令进行缩放,参数是x轴方向和y轴方向的缩放倍数(大于1放大,小于1缩小),还可以指定中心坐标,默认是几何中心。
Canvas(modifier = Modifier.fillMaxSize()) {
scale(scaleX = 10f, scaleY = 15f) {
drawCircle(Color.Blue, radius = 20.dp.toPx())
}
}
位移
DrawScope.translate可以实现位移,参数是x方向或者y方向的距离。参数为正,是沿着坐标轴正向,为负就是反向。
Canvas(modifier = Modifier.fillMaxSize()) {
translate(left = 100f, top = -300f) {
drawCircle(Color.Blue, radius = 200.dp.toPx())
}
}
旋转
用DrawScope.rotate函数实现旋转,参数为正时是顺时针的角度,为负就是逆时针,可以指定中心点,默认是几何中心。
Canvas(modifier = Modifier.fillMaxSize()) {
rotate(degrees = 45F) {
drawRect(
color = Color.Gray,
topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
size = size / 3F
)
}
}
画布尺寸调整
用DrawScope.inset函数来对DrawScope的画布进行调整,参数是周围四个方向的边距偏移量。
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasQuadrantSize = size / 2F
inset(horizontal = 50f, vertical = 30f) {
drawRect(color = Color.Green, size = canvasQuadrantSize)
}
}
这样调整后,inset内部的lambda中的绘制指令的尺寸size会受影响,size.width = width - 2 * horizontal,size.height = height - 2 * vertical,相当于是加了padding。
组合变幻
变幻除了可以单独使用,还可以组合起来使用,能更简便的实现变幻效果。使用DrawScope.withTransform来组合变幻:
Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({
translate(left = size.width / 5F)
rotate(degrees = 45F)
}) {
drawRect(
color = Color.Gray,
topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
size = size / 3F
)
}
}
总结
今天主要学习了如何通过Canvas函数来实现自定义绘制内容,Canvas给我们了封装了一个包含有DrawScope的lambda,通过DrawScope提供的各种绘制指令可以实现我们的想要的自定义内容。可以自由的通过绘制图形,文字和图像,并且可以做变幻,以实现一些特效。相信通过今天的学习,足可以应付常见的自定义绘制需求。
参考资料
- Graphics in Compose
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!