降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

  • Graphics modifiers

subscription

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

保护原创,请勿转载!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/800576.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot+Vue实现简单的文件上传(Excel篇)

SpringBootVue实现简单的文件上传 1 环境 SpringBoot 3.2.1&#xff0c;Vue 2&#xff0c;ElementUI 2 页面 3 效果&#xff1a;只能上传xls文件且大小限制为2M&#xff0c;选择文件后自动上传。 4 前端代码 <template><div class"container"><el…

Python爬虫速成之路(4):BeautifulSoup

hello hello~ &#xff0c;这里是绝命Coding——老白~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#xff1a;绝命Coding-CSDN博客 &a…

【JVM基础01】——介绍-初识JVM运行流程

目录 1- 引言&#xff1a;初识JVM1-1 JVM是什么&#xff1f;(What)1-1-1 概念1-1-2 优点 1-2 为什么学习JVM?(Why) 2- 核心&#xff1a;JVM工作的原理&#xff08;How&#xff09;⭐2-1 JVM 的组成部分及工作流程2-2 学习侧重点 3- 小结(知识点大纲)&#xff1a;3-1 JVM 组成3…

实践致知第16享:设置Word中某一页横着的效果及操作

一、背景需求 小姑电话说&#xff1a;现在有个word文档,里面有个表格太长&#xff08;如下图所示&#xff09;&#xff0c;希望这一个设置成横的&#xff0c;其余页还是保持竖的&#xff01; 二、解决方案 1、将鼠标放置在该页的最前面闪烁&#xff0c;然后选择“页面”》“↘…

京东.Vision首登苹果Vision Pro 背后的技术探索

去年6月&#xff0c;苹果正式发布首款头显设备Apple Vision Pro&#xff0c;今年6月28号&#xff0c;Apple Vision Pro正式在中国发售。京东.Vision作为首批原生应用登陆Vision Pro平台&#xff0c;首期以家电家居与潮流数码产品作为切入口&#xff0c;未来将逐步拓展至全品类&…

07:串口通信二

串口编程 1、与波特率之相关的寄存器2、PCON寄存器3、SCON寄存器4、配置的代码分析5、向PC发送一段字符串6、PC机向单片机发送字符控制LED1灯的亮灭 1、与波特率之相关的寄存器 如图&#xff0c;与串口通信相关的寄存器主要是SCON和PCON寄存器。 2、PCON寄存器 SMOD&#xff1…

CentOS搭建邮件服务器:DNS配置方法技巧?

CentOS搭建邮件服务器的流程&#xff1f;如何高效使用CentOS&#xff1f; 在当今数字化时代&#xff0c;邮件服务器的需求日益增加。为了确保邮件能够顺利送达&#xff0c;正确的DNS配置是必不可少的一环。AokSend将详细介绍在CentOS搭建邮件服务器过程中&#xff0c;如何进行…

PyCharm软件初始化配置

安装完pycharm后&#xff0c;需要对其进行个性化设置&#xff0c;分别设置方法如下 目录 一、修改主题二、修改默认字体和大小三、设置拖动滚轮改变字体大小四、常见快捷键 一、修改主题 1、界面右上角点击红框的内容 2、选择Theme选项 3、选择对应的主题 第一二个是白色主题…

怎样去除视频上的水印和文字,视频水印文本移除教程

在观看和分享视频时&#xff0c;我们经常会遇到带有水印或额外文字的情况。这些标记有时是为了版权保护&#xff0c;有时则是平台的标识&#xff0c;但在某些情况下&#xff0c;它们可能会干扰视频的观赏体验。本文将向你介绍常见的视频水印类型以及如何使用简鹿水印助手去除这…

让AI语言模型自由飞翔:LangChain框架的奇妙世界

今天&#xff0c;我将为大家揭开一项令人激动的技术——LangChain。想象一下&#xff0c;如果能将人工智能的强大能力与我们日常使用的数据和工具无缝连接&#xff0c;那将开启怎样崭新且无限的可能&#xff01; LangChain&#xff0c;一个专为大型语言模型设计的框架&#xf…

C语言 ——— 调试的时候如何查看当前程序的变量信息

目录 调试前/后的调试窗口 ​编辑 调试窗口 --- 监视 调试窗口 --- 内存 调试窗口 --- 调用堆栈 调试前/后的调试窗口 调试前的调试窗口&#xff1a; 调试前的调试窗口是没有显示的&#xff0c;只有在调试的时候才会有相对应的调试窗口 调试后的调试窗口&#xff1a…

windows下环境变量开启方式

第一种方法&#xff1a; 使用快捷键打开“运行”对话框&#xff1a;按下 Win R 组合键&#xff0c;这将打开“运行”窗口。 输入系统属性命令&#xff1a;在“运行”窗口中输入 sysdm.cpl 然后按回车键。这将打开“系统属性”对话框。【sysdm.cpl是"System Data Manager…

关于java的反射

❓❓❓反射是啥呀相信许多学java的同学非常困惑在学的时候&#xff0c;总是感觉懂了却又没懂或者直接忽略过去了&#xff0c;那么本文就带大家探讨一下什么是反射在java中以及它的机制和运用。 ⭐️什么是反射&#xff1a; 首先我们知道一些知识&#xff1a; 维基百科的解释 …

C++ | Leetcode C++题解之第235题二叉搜索树的最近公共祖先

题目&#xff1a; 题解&#xff1a; class Solution { public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {TreeNode* ancestor root;while (true) {if (p->val < ancestor->val && q->val < ancestor->val) {anc…

Linux FFmpeg安装教程

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

白平衡说明

白平衡 相机白平衡的起源原理以及作用起源作用 白平衡的原理白平衡的类型应用说明 工业相机的白平衡效果对比一键白平衡的必要性一键白平衡的实现方式 相机白平衡的起源原理以及作用 起源 白平衡&#xff08;White Balance, WB&#xff09;概念的起源与色温理论密切相关。色温…

【SpringBoot】SpringCache轻松启用Redis缓存

目录&#xff1a; 1.前言 2.常用注解 3.启用缓存 1.前言 Spring Cache是Spring提供的一种缓存抽象机制&#xff0c;旨在通过简化缓存操作来提高系统性能和响应速度。Spring Cache可以将方法的返回值缓存起来&#xff0c;当下次调用方法时如果从缓存中查询到了数据&#xf…

python如何判断变量是否可迭代

python如何判断变量是否可迭代&#xff1f;方法如下&#xff1a; 方法一&#xff1a; 适用于python2和python3 >>> from collections import Iterable >>> isinstance("str", Iterable) True 方法二&#xff1a; 适用于python3 s "hello …

仿RabbitMQ消息队列

项目介绍 介绍 本项目已上传&#xff0c;后期会做扩展&#xff1a;Gitee获取完整项目源码 该项目是仿照RabitMQ实现简版的消息队列。主要是解决了普通生产消费者模型只能在单主机上生产消费模型的缺点。该项目是可以进行跨网络传输生产与消费&#xff0c;实现不同主机间的数…

C语言中的指针:掌握内存的钥匙

C语言中的指针&#xff1a;掌握内存的钥匙 引言 C语言是一种结构化编程语言&#xff0c;它提供了对硬件底层的直接访问&#xff0c;其中最强大的特性之一就是指针。指针允许程序员直接操作内存地址&#xff0c;这对于理解程序的内部工作原理以及优化代码性能至关重要。本文将深…