公众号「稀有猿诉」 原文链接 降Compose十八掌之『突如其来』| Graphics Modifiers
在Jetpack Compose中创建自定义绘制内容的方式不止一种,除了前面提到的通过Canvas函数的方式以外,还可以通过Modifier的几个扩展函数更为灵活实现一些的自定义内容。今天就来学习一下如何使用Modifier的扩展函数来绘制自定义内容。
使用Modifier来叠加自定义内容
先用一个简单的实例来看一下,如何用Modifier来实现一个自定义内容:
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,它可以让你在目标Composable的内容绘制前或者绘制后,执行一些DrawScope的绘制命令来进行自定义的绘制。也就是说,这个扩展函数可以你让自由的决定在目标Composable绘制之前前或者绘制之后,执行自己想要的绘制命令,以实现一些额外的自定义效果。不过,要记得调用drawContent函数,这个函数是目标Composable的内容绘制函数,当然也可以不调用,那样就变成纯的自定义Composable了。
来看一个猫眼效果:
@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(0xFF9E8240),
Color(0xFF42A565),
Color(0xFFE2E575)
)
)
onDrawBehind {
drawRoundRect(
brush,
cornerRadius = CornerRadius(10.dp.toPx())
)
}
}
.padding(16.dp),
style = MaterialTheme.typography.headlineMedium
)
}
}
背景式绘制
Modifier.drawBehind是在目标Composable内容的下面一层(更远离用户的方向)执行绘制命令,所以方便添加一些背景:
Box {
Text(
"降Compose十八掌!",
modifier = Modifier
.padding(16.dp)
.drawBehind {
drawRoundRect(
Color(0xFFBBAAEE),
cornerRadius = CornerRadius(10.dp.toPx())
)
}
.padding(8.dp),
style = MaterialTheme.typography.headlineLarge
)
}
缓存式绘制
Modifier.drawWithCache能够缓存在lambda内部创建的一些对象,这主要是为了提升性能的。有过View经验的同学一定知道在自定义View的时候不能在onDraw里面创建对象,因为这会影响性能。这个函数的用途也在于此,把一些对象缓存起来,避免多次创建,以提升渲染性能。
需要注意的是,这些缓存对象的生命周期是画面尺寸未改变,以及创建对象依赖的状态没有变化,也就是说一旦画面有改变,或者依赖的状态有变化,那么缓存失效,对象要被重新创建。
注意,这个函数主要用于与绘制命令强相关的,或者说仅在绘制命令范围内使用的对象,如颜色啊,画刷(Brush),着色器(Shader)啊,路径(Path)啊之类的。
Box {
Text(
"降Compose十八掌!",
modifier = Modifier
.padding(16.dp)
.drawWithCache {
val brush = Brush.linearGradient(
listOf(
Color(0xFF9E82F0),
Color(0xFF42A5F5),
Color(0xFFE2E575)
)
)
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三个参数,通过一个例子来感受这些变幻效果:
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(0xFF9E82F0),
Color(0xFF42A5F5),
Color(0xFFE2E575)
)
)
onDrawBehind {
drawRoundRect(
brush,
cornerRadius = CornerRadius(10.dp.toPx())
)
}
}
.padding(16.dp),
style = MaterialTheme.typography.headlineLarge
)
}
剪辑与形状
剪辑(clip)是把绘制好的图层进行裁剪,裁剪的效果由形状(shape)来指定。这里可以尽情的发挥想像力,做出非常炫酷的视觉效果。
Box(
modifier = Modifier
.size(200.dp)
.graphicsLayer {
clip = true
shape = CircleShape
}
.background(Color(0xFFF06292))
) {
Text(
"降Compose十八掌",
style = TextStyle(color = Color.Black, fontSize = 36.sp),
modifier = Modifier.align(Alignment.Center)
)
}
图层的变幻仅对绘制生效
需要注意的是,对图层做的变幻仅是对渲染结果生效,它并不影响Composable本身的属性(如大小和位置)。比如说,通过剪辑和位移,图层可能会超出Composable本身的区域,也就是说在View树中,这个元素的位置和大小还是原来的样子。
通过Modifier中其他的函数能对Composable本身进行剪辑这才会真正影响它自身的大小,超出边界的内容会被裁剪掉:
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(0xFFF06292))
) {
Text(
"降Compose十八掌",
style = TextStyle(color = Color.Black, fontSize = 36.sp),
modifier = Modifier.align(Alignment.Center)
)
}
Box(
modifier = Modifier
.size(200.dp)
.background(Color(0xFF4DB6AC))
)
}
创建Composable的快照
就像截屏一样,可以给Composable拍照,即把Composable的绘制结果转成一个Bitmap,进而可以保存成图片文件,或者分享到其他应用。主要是通过graphicsLayer的record函数:
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的一层包装:
@Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
Spacer(modifier.drawBehind(onDraw))
因为Spacer是一个空白的占位符,本身的内容就是空的(只有大小,没有内容),所以整体效果就相当于是一个纯的自定义绘制内容了。
不过本质上都是使用DrawScope对象来进行具体的绘制,上面提到的Modifier的扩展函数也都是对DrawScope的封装。Modifier的强大之处在于它可以应用于所有其他的Composables,可以让开发者非常方便的对现有的Composables进行扩展和增强。
References
- Graphics modifiers
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!