在Jetpack Compose中创建自定义绘制内容的方式不止一种,除了前面提到的通过Canvas函数的方式 以外,还可以通过Modifier的几个扩展函数更为灵活实现一些的自定义内容。今天就来学习一下如何使用Modifier的扩展函数来绘制自定义内容。
使用Modifier来叠加自定义内容
先用一个简单的实例来看一下,如何用Modifier来实现一个自定义内容:
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
val textMeasurer = rememberTextMeasurer ()
Box (
modifier = Modifier . fillMaxSize ()
. padding ( 16. dp )
. drawWithContent {
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 )
)
}
)
可以看到使用Modifier方式与Canvas略不一样,它要应用到其他的Composable上面,所以Modifier方式主要用于修改或者增强 现有的Composable以达到想要的效果。仍是提供了一个带有DrawScope 指针的lambda,在这里写绘制指令。
Modifier提供的自定义绘制方式有四种:drawWithContent,drawBehind,drawWithCache和graphicsLayer。前面三种是是针对绘制的扩展,也就是影响绘制的内容;最后一个是图形的扩展,也就是主要用于已经绘制好了的内容的变幻。
覆写式绘制
最核心的扩展函数就是Modifier.drawWithContent .drawWithContent(kotlin.Function1)),它可以让你在目标Composable的内容绘制前或者绘制后,执行一些DrawScope的绘制命令来进行自定义的绘制。也就是说,这个扩展函数可以你让自由的决定在目标Composable绘制之前前或者绘制之后,执行自己想要的绘制命令,以实现一些额外的自定义效果。不过,要记得调用drawContent函数,这个函数是目标Composable的内容绘制函数,当然也可以不调用,那样就变成纯的自定义Composable了。
来看一个猫眼效果:
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Composable
fun DrawContentDemo ( modifier : Modifier = Modifier . fillMaxSize ()) {
var pointerOffset by remember {
mutableStateOf ( Offset ( 0f , 0f ))
}
Column (
modifier = Modifier
. fillMaxSize ()
. pointerInput ( "dragging" ) {
detectDragGestures { change , dragAmount ->
pointerOffset += dragAmount
}
}
. onSizeChanged {
pointerOffset = Offset ( it . width / 2f , it . height / 2f )
}
. drawWithContent {
drawContent ()
// draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI.
drawRect (
Brush . radialGradient (
listOf ( Color . Transparent , Color . Black ),
center = pointerOffset ,
radius = 100. dp . toPx (),
)
)
}
) {
Text (
text =
"""
“降龙十八掌可说是【武学中的巅峰绝诣】,当真是无坚不摧、无固不破。虽招数有限,但每一招均具绝大威力。
北宋年间,丐帮帮主萧峰以此邀斗天下英雄,极少有人能挡得他三招两式,气盖当世,群豪束手。
当时共有“降龙廿八掌”,后经萧峰及他义弟虚竹子删繁就简,取精用宏,改为降龙十八掌,掌力更厚。
这掌法传到洪七公手上,在华山绝顶与王重阳、黄药师等人论剑时施展出来,王重阳等尽皆称道。”
""".trimIndent(),
modifier = Modifier
. padding ( 16. dp )
. drawWithCache {
val brush = Brush . linearGradient (
listOf (
Color ( 0 xFF9E8240 ),
Color ( 0 xFF42A565 ),
Color ( 0 xFFE2E575 )
)
)
onDrawBehind {
drawRoundRect (
brush ,
cornerRadius = CornerRadius ( 10. dp . toPx ())
)
}
}
. padding ( 16. dp ),
style = MaterialTheme . typography . headlineMedium
)
}
}
背景式绘制
Modifier.drawBehind .drawBehind(kotlin.Function1))是在目标Composable内容的下面一层(更远离用户的方向)执行绘制命令,所以方便添加一些背景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Box {
Text (
"降Compose十八掌!" ,
modifier = Modifier
. padding ( 16. dp )
. drawBehind {
drawRoundRect (
Color ( 0 xFFBBAAEE ),
cornerRadius = CornerRadius ( 10. dp . toPx ())
)
}
. padding ( 8. dp ),
style = MaterialTheme . typography . headlineLarge
)
}
缓存式绘制
Modifier.drawWithCache .drawWithCache(kotlin.Function1))能够缓存在lambda内部创建的一些对象,这主要是为了提升性能的。有过View经验的同学一定知道在自定义View的时候不能在onDraw里面创建对象,因为这会影响性能。这个函数的用途也在于此,把一些对象缓存起来,避免多次创建,以提升渲染性能。
需要注意的是,这些缓存对象的生命周期是画面尺寸未改变,以及创建对象依赖的状态没有变化,也就是说一旦画面有改变,或者依赖的状态有变化,那么缓存失效,对象要被重新创建。
注意,这个函数主要用于与绘制命令强相关的,或者说仅在绘制命令范围内使用的对象,如颜色啊,画刷(Brush),着色器(Shader)啊,路径(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
Box {
Text (
"降Compose十八掌!" ,
modifier = Modifier
. padding ( 16. dp )
. drawWithCache {
val brush = Brush . linearGradient (
listOf (
Color ( 0 xFF9E82F0 ),
Color ( 0 xFF42A5F5 ),
Color ( 0 xFFE2E575 )
)
)
onDrawBehind {
drawRoundRect (
brush ,
cornerRadius = CornerRadius ( 10. dp . toPx ())
)
}
}
. padding ( 16. dp ),
style = MaterialTheme . typography . headlineLarge
)
}
还要注意与状态(State)的区别,使用remember函数可以创建状态,这些状态的生命周期也是能跨越函数的,这也相当于是缓存。但状态的目的是让Compose感知数据变化,进面进行重组(ReComposition)。把与绘制强相关的对象放在状态里面(即用remember转成状态)并不合适。因为与绘制强相关的对象如Brush,Color和Shader等,它并不是自变量,而是因变量,这些对象依赖其底层的数据变化而需要重新创建。所以,最恰当的方式是,是把自变量如底层的颜色数值,或者图片放到状态里面,而Brush和Shader放在drawWithCache里面。
图形变幻
Modifier.graphicsLayer ))是一个图形的扩展函数,它能够把目标Composable的内容绘制到一个图层(layer)上面,然后提供了一些针对图层进行操作的函数,进而能实现一些变幻。这相当于是把绘制指令做了隔离,先把绘制结果放到一个图层上面,除了变幻,图层还能做很多事情:
做类似于RenderNode 那样的渲染管线化(render pipeline),把图层用作管理线中的一个节点,而不用每次都重新绘制。
光栅化(Rasterization),图层可以光栅化,甚至离屏渲染(offscreen drawing),这可以优化动画的帧率和流畅度。
不过,最主要的仍是做变幻,进而实现动画(Animation)。但要注意,图形变幻,仅是针对绘制过程做的变幻,并不影响Composable的真实的属性。
graphicsLayer也是一个扩展函数,它的lambda参数是GraphicsLayerScope的一个扩展函数,所以lambda中有指向GraphicsLayerScope的隐式指针。变幻,只需要指定一些参数的值即可,通过一些例子,一看就能懂。
缩放/位移/旋转/透明度
通过在graphicsLayer的lambda中指定相应的参数即可以实现这些变幻。对于旋转和缩放,还可以指定中心点(Origin),特别注意旋转,它是三维的有x,y,z三个参数,通过一个例子来感受这些变幻效果:
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
Box (
modifier = Modifier
. graphicsLayer {
scaleX = 1.1f
scaleY = 1.6f
translationX = 30. dp . toPx ()
translationY = 50. dp . toPx ()
alpha = 0.7f
rotationX = 10f
rotationY = 5f
}
) {
Text (
"降Compose十八掌!" ,
modifier = Modifier
. padding ( 16. dp )
. drawWithCache {
val brush = Brush . linearGradient (
listOf (
Color ( 0 xFF9E82F0 ),
Color ( 0 xFF42A5F5 ),
Color ( 0 xFFE2E575 )
)
)
onDrawBehind {
drawRoundRect (
brush ,
cornerRadius = CornerRadius ( 10. dp . toPx ())
)
}
}
. padding ( 16. dp ),
style = MaterialTheme . typography . headlineLarge
)
}
剪辑与形状
剪辑(clip)是把绘制好的图层进行裁剪,裁剪的效果由形状(shape)来指定。这里可以尽情的发挥想像力,做出非常炫酷的视觉效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Box (
modifier = Modifier
. size ( 200. dp )
. graphicsLayer {
clip = true
shape = CircleShape
}
. background ( Color ( 0 xFFF06292 ))
) {
Text (
"降Compose十八掌" ,
style = TextStyle ( color = Color . Black , fontSize = 36. sp ),
modifier = Modifier . align ( Alignment . Center )
)
}
图层的变幻仅对绘制生效
需要注意的是,对图层做的变幻仅是对渲染结果生效,它并不影响Composable本身的属性(如大小和位置)。比如说,通过剪辑和位移,图层可能会超出Composable本身的区域,也就是说在View树中,这个元素的位置和大小还是原来的样子。
通过Modifier中其他的函数能对Composable本身进行剪辑这才会真正影响它自身的大小,超出边界的内容会被裁剪掉:
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
Column ( modifier = Modifier . fillMaxSize (). padding ( 16. dp )) {
Box (
modifier = Modifier
. size ( 200. dp )
. clip ( RectangleShape )
. border ( 2. dp , Color . Black )
. graphicsLayer {
clip = true
shape = CircleShape
translationX = 50. dp . toPx ()
translationY = 50. dp . toPx ()
}
. background ( Color ( 0 xFFF06292 ))
) {
Text (
"降Compose十八掌" ,
style = TextStyle ( color = Color . Black , fontSize = 36. sp ),
modifier = Modifier . align ( Alignment . Center )
)
}
Box (
modifier = Modifier
. size ( 200. dp )
. background ( Color ( 0 xFF4DB6AC ))
)
}
创建Composable的快照
就像截屏一样,可以给Composable拍照,即把Composable的绘制结果转成一个Bitmap,进而可以保存成图片文件,或者分享到其他应用。主要是通过graphicsLayer的record函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val coroutineScope = rememberCoroutineScope ()
val graphicsLayer = rememberGraphicsLayer ()
Box (
modifier = Modifier
. drawWithContent {
// 用record函数来录制图层
graphicsLayer . record {
// 把内容绘制到图层上面
this @drawWithContent . drawContent ()
}
// 把图层再绘制到画布上面,以让内容能正常显示
drawLayer ( graphicsLayer )
}
. clickable {
coroutineScope . launch {
val bitmap = graphicsLayer . toImageBitmap ()
// 快照Bitmap已准备好了,可以使用此Bitmap了
}
}
. background ( Color . White )
) {
Text ( "Hello Android" , fontSize = 26. sp )
}
注意:函数rememberGraphicsLayer只在compose的1.7.0-alpha07以后的版本才支持,在稳定版本中是不支持的。以BOM方式指定的依赖都是稳定版。可以单独给compose-ui:ui指定版本,如implementation(“androidx.compose.ui:ui:1.7.0-beta03”)
如何选择恰当的方式
自定义绘制有两种,一种纯的自已绘制内容,类似于直接继承View,在onDraw中绘制自己想要的效果;另外一种就是基于现有的部件进行改进和增强,类似于子例化TextView或者子例化ImageView,基于原View的内容,再进行变幻,改进或者增强。
视具体的问题而定,如果是第一种,就用Canvas函数,否则的话就用上面讲的Modifier的扩展函数。
其实如果仔细看API的实现,就可以发现Canvas函数其实是Modifier.drawBehind的一层包装:
1
2
3
@Composable
fun Canvas ( modifier : Modifier , onDraw : DrawScope .() -> Unit ) =
Spacer ( modifier . drawBehind ( onDraw ))
因为Spacer是一个空白的占位符,本身的内容就是空的(只有大小,没有内容),所以整体效果就相当于是一个纯的自定义绘制内容了。
不过本质上都是使用DrawScope对象来进行具体的绘制,上面提到的Modifier的扩展函数也都是对DrawScope的封装。Modifier的强大之处在于它可以应用于所有其他的Composables,可以让开发者非常方便的对现有的Composables进行扩展和增强。
References