任何一个GUI框架都会提供大量的预定义的UI部件,让开发者构建UI页面,但有些时候预定义的部件无法满足需求,这时就需要定制,甚至是自定义绘制的内容。对于Android开发者来说,这已经是家常便饭了,因为肯定有过用自定义View来实现一些特殊设计需求的经验。在Jetpack Compose中也有同样的方法来实现自定义绘制内容,今天就来学习一下。
使用Canvas来自定义内容
在Compose中, 我们用Canvas函数 )来绘制自定义内容,可以把它理解成为自定义View,但,它是一个函数,把绘制指令传给它就可以了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)是最为常见的一类绘制目标,比如圆,椭圆,矩形,线,扇形等等。不难,看一眼就会用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)是把一系列的数学指令转化为绘制命令,可以更为灵活的画一些曲线和图形,比如说画一个三角函数曲线:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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中有提供绘制图片的方法:
1
2
3
4
val dogImage = ImageBitmap . imageResource ( id = R . drawable . dog )
Canvas ( modifier = Modifier . fillMaxSize (), onDraw = {
drawImage ( dogImage )
})
变幻
除了绘制以外,DrawScope还提供了一系列做变幻的函数。包括缩放,位移,旋转这些变幻直接作用于绘制指令上面。
缩放
使用DrawScope.scale .scale(kotlin.Float,kotlin.Float,androidx.compose.ui.geometry.Offset,kotlin.Function1))函数来对绘制指令进行缩放,参数是x轴方向和y轴方向的缩放倍数(大于1放大,小于1缩小),还可以指定中心坐标,默认是几何中心。
1
2
3
4
5
Canvas ( modifier = Modifier . fillMaxSize ()) {
scale ( scaleX = 10f , scaleY = 15f ) {
drawCircle ( Color . Blue , radius = 20. dp . toPx ())
}
}
位移
DrawScope.translate .translate(kotlin.Float,kotlin.Float,kotlin.Function1))可以实现位移,参数是x方向或者y方向的距离。参数为正,是沿着坐标轴正向,为负就是反向。
1
2
3
4
5
Canvas ( modifier = Modifier . fillMaxSize ()) {
translate ( left = 100f , top = - 300f ) {
drawCircle ( Color . Blue , radius = 200. dp . toPx ())
}
}
旋转
用DrawScope.rotate .rotate(kotlin.Float,androidx.compose.ui.geometry.Offset,kotlin.Function1))函数实现旋转,参数为正时是顺时针的角度,为负就是逆时针,可以指定中心点,默认是几何中心。
1
2
3
4
5
6
7
8
9
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 .inset(kotlin.Float,kotlin.Float,kotlin.Float,kotlin.Float,kotlin.Function1))函数来对DrawScope的画布进行调整,参数是周围四个方向的边距偏移量。
1
2
3
4
5
6
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 .withTransform(kotlin.Function1,kotlin.Function1))来组合变幻:
1
2
3
4
5
6
7
8
9
10
11
12
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提供的各种绘制指令可以实现我们的想要的自定义内容。可以自由的通过绘制图形,文字和图像,并且可以做变幻,以实现一些特效。相信通过今天的学习,足可以应付常见的自定义绘制需求。
参考资料