降Compose十八掌之『突如其来』| Graphics Modifiers

公众号「稀有猿诉」        原文链接 降Compose十八掌之『突如其来』| Graphics Modifiers

在Jetpack Compose中创建自定义绘制内容的方式不止一种,除了前面提到的通过Canvas函数的方式以外,还可以通过Modifier的几个扩展函数更为灵活实现一些的自定义内容。今天就来学习一下如何使用Modifier的扩展函数来绘制自定义内容。

banner

使用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)
                )
            }
    )

hello_graphics_modifer.png

可以看到使用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
        )
    }
}

draw_content.gif

背景式绘制

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
        )
    }

drawcache_demo.png

还要注意与状态(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
        )
    }

graphics_layer_transform.png

剪辑与形状

剪辑(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)
            )
        }

clip_shape.png

图层的变幻仅对绘制生效

需要注意的是,对图层做的变幻仅是对渲染结果生效,它并不影响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))
        )
    }

clip_modifier.png

创建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”)

graphics_to_bitmap.png

如何选择恰当的方式

自定义绘制有两种,一种纯的自已绘制内容,类似于直接继承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

subscription

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

  • 15
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值