Jetpack Compose(十五)Compose组件渲染流程-绘制
作者:访客发布时间:2023-12-28分类:程序开发学习浏览:117
绘制阶段主要是将所有LayoutNode实际绘制到屏幕之上,也可以对绘制阶段进行定制。如果我们对Android原生Canvas已经非常熟悉,迁移到Compose是没有任何学习成本的。即使从未接触过也没有关系,在Compose中,官方为我们提供了大量简单且实用的基础绘制API,能够满足绝大多数场景下的定制需求,通过本节的学习,我们将具备扎实的组件绘制定制能力。
一、Canvas Composable
Canvas Composable是官方提供的一个专门用来自定义绘制的单元组件。之所以说是单元组件,是因为这个组件不可以包含任何子组件,可以看作是传统View系统中的一个单元View。
CanvasComposable包含两个参数,一个是Modifier,另一个是DrawScope作用域代码块。
@Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
Spacer(modifier.drawBehind(onDraw)) //所有的绘制逻辑最终都传入drawBehind()修饰符方法里
在DrawScope作用域中,Compose提供了基础绘制API,如表所示:
API | 描述 |
---|---|
drawLine | 绘制线 |
drawRect | 绘制矩形 |
drawlmage | 绘制图片 |
drawRoundRect | 绘制圆角矩形 |
drawCircle | 绘制圆 |
drawOval | 绘制椭圆 |
drawArc | 绘制弧线 |
drawPath | 绘制路径 |
drawPoints | 绘制点 |
接下来我们通过绘制API成完一个简单的圆形加载进度条组件,如图所示:
加载进度组件绘制起来并不复杂,可以通过圆环与圆弧的叠加进行实现。完整代码如下:
@Composable
fun Greeting() {
//初始进度
var progress by remember { mutableStateOf(0F) }
//展示进度条
LoadingProgressBar(progress)
//使用Timer模拟
LaunchedEffect(Unit) {
timer(period = 100) {
if (progress < 100F) {
progress += 1F
}
}
}
}
/**
* 加载Loading圆形进度条
* @param progress 进度
*/
@Composable
fun LoadingProgressBar(progress: Float) {
val loadingText = "Loading"
//将绘制的角度平滑过渡
val sweepAngle = animateFloatAsState(
targetValue = progress / 100F * 360F,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, label = ""
).value
Box(
modifier = Modifier
.size(300.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = loadingText,
color = Color.Black,
fontSize = 28.sp,
fontWeight = FontWeight.Bold
)
Text(
text = "${progress.toInt()}%",
color = Color.Black,
fontSize = 28.sp,
fontWeight = FontWeight.Bold
)
}
Canvas(
modifier = Modifier
.fillMaxSize()
.padding(30.dp)
) {
//绘制圆,drawContext.size当前绘制环境的尺寸
drawCircle(
color = Color(0xFF1E7171),
center = Offset(drawContext.size.width / 2f, drawContext.size.height / 2f),
style = Stroke(width = 20.dp.toPx())
)
//绘制圆弧
drawArc(
color = Color(0xFF3BDCCE),
startAngle = -90f,
sweepAngle = sweepAngle,
useCenter = false,
style = Stroke(width = 20.dp.toPx(), cap = StrokeCap.Round)
)
}
}
}
代码并没有很复杂的地方,带着注释大概都能看懂,其中animateFloatAsState是让值平滑的过渡,绘制的UI也会看起来更加丝滑,如上面UI图所示。
查阅Canvas组件的实现,可以发现其本质上就是一个Spacer,所有的绘制逻辑最终都传入drawBehind()修饰符方法里。这个API字面意思很明确,绘制在后面即绘制在底部图层。由于该修饰符方法修饰在Spacer上,这表明我们其实是在Spacer的底部图层上完成的定制绘制。由于Spacer背景是透明的,所以绘制的内容就完全展示出来了。
接下来看看其他一些与绘制相关的修饰符方法。由于这些修饰符方法返回的都是DrawModifier的子类,所以将这些修饰符统称为DrawModifier类修饰符方法。
二、DrawModifier
DrawModifier类修饰符方法共有三个,每个都有其各自的使命。drawWithContent允许开发者可以在绘制时自定义绘制层级,Canvas Composable中使用的drawBehind是用来定制绘制组件背景的,而drawWithCache则允许开发者在绘制时可以携带缓存。接下来学习这三个API该如何正确使用。
1、drawWithContent
先来看看drawWithContent,这个API允许开发者在绘制时自定义绘制层级,那么什么是绘制层级呢?其实就是越先绘制的内容Z轴越小,后面绘制的内容可能会遮盖前面绘制的内容,这样就产生了绘制的层级关系。通过API声明,可以看到drawWithContent需要一个ContentDrawScope作用域Lambda,而这个ContentDrawScope实际上就是在DrawScope作用域基础上拓展了一个drawContent。
//drawWithContent
fun Modifier.drawWithContent(
onDraw: ContentDrawScope.() -> Unit
): Modifier = this then DrawWithContentElement(onDraw)
//ContentDrawScope
@JvmDefaultWithCompatibility
interface ContentDrawScope : DrawScope {
fun drawContent()
}
Modifier是修饰某个具体组件的,drawContent的作用就是绘制组件本身的内容。例如Text,组件本身会绘制一串文本。当我们想为这个文本绘制背景色时,就需要先绘制背景色再绘制文本,在传统View中会像这样
class CustomTextView(context: Context) : AppCompatTextViw(context) {
override fun onDraw(canvas: Canvas?) {
//在TextView下层绘制
super.onDraw(canvas)
//在TextView上层绘制
}
}
而这与drawContent的设计是相通的。
2、drawBehind
了解了drawContent,drawBehind就很好理解了就是用来自定义绘制组件背景的。
在drawBehind的实现源码中,定制的绘制逻辑onDraw会被传入DrawBackgroundModifier的主构造器中。在重写的draw方法中,首先调用了我们传入的定制绘制逻辑,之后调用drawContent来绘制组件内容本身。
fun Modifier.drawBehind(
onDraw: DrawScope.() -> Unit
) = this then DrawBehindElement(onDraw) //DrawBehindElement
@OptIn(ExperimentalComposeUiApi::class)
private data class DrawBehindElement( //DrawBehindElement
val onDraw: DrawScope.() -> Unit
) : ModifierNodeElement<DrawBackgroundModifier>() {
override fun create() = DrawBackgroundModifier(onDraw) //DrawBackgroundModifier
...
}
@OptIn(ExperimentalComposeUiApi::class)
private class DrawBackgroundModifier( //DrawBackgroundModifier
var onDraw: DrawScope.() -> Unit
) : Modifier.Node(), DrawModifierNode {
override fun ContentDrawScope.draw() {
onDraw() //先绘制背景
drawContent() //再绘制内容
}
}
我们看一下二个Api如何使用以及更直观的通过UI查看二者的区别,代码如下:
@Composable
fun Greeting() {
Column {
Spacer(modifier = Modifier
.fillMaxWidth()
.height(30.dp))
DrawBefore()
Spacer(modifier = Modifier
.fillMaxWidth()
.height(30.dp))
DrawBehind()
}
}
@Composable
fun DrawBefore() {
Box {
Card(
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.size(100.dp)
.drawWithContent {
drawContent() //绘制内容
drawCircle( //在内容上绘制红点
color = Color.Red,
18.dp.toPx() / 2,
center = Offset(drawContext.size.width, 0f)
)
}) {
Image(painter = painterResource(id = R.mipmap.rabit2), contentDescription = null)
}
}
}
@Composable
fun DrawBehind() {
Box {
Card(
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.size(100.dp)
.drawBehind { //绘制背景
drawCircle( //绘制小圆点
color = Color.Red,
18.dp.toPx() / 2,
center = Offset(drawContext.size.width, 0f)
)
}) {
Image(painter = painterResource(id = R.mipmap.rabit2), contentDescription = null)
}
}
}
UI效果
3、drawWithCache
有时在DrawScope中绘制时,会用到一些与绘制有关的对象(如ImageBitmap、Paint、Path等),当组件发生重绘时,由于DrawScope会反复执行,这其中声明的对象也会随之重新创建,实际上这类对象是没必要重新创建的。如果这类对象占用内存空间较大,频繁多次重绘意味着这类对象会频繁地加载重建,从而导致内存抖动等问题。
也许有人会提出疑问,将这类对象存放到外部Composable作用域中,并利用remember缓存不可以吗?当然这个做法从语法上来说是可行的,但这样做违反了迪米特法则,这类对象可能会被同Composable内其他组件依赖使用。如果将这类对象存放到全局静态域会更危险,不仅会污染全局命名空间,并且当该Composable组件离开视图树时,还会导致内存泄漏问题。由于这类对象只跟这次绘制有关,所以还是放在一块比较合适。
补充提示:
迪米特法则,又称最少知识原则,是面向对象五大设计原则之一。它规定每个类应对其他类尽可能少了解。如果两个类不必直接相互通信,便采用第三方类进行转发,尽可能减小类与类之间的耦合度。
为解决这个问题,Compose为我们提供了drawWithCache方法,就是支持缓存的绘制方法。通过drawWithCache声明可以看到,需要一个传入CacheDrawScope作用域的Lambda,值得注意的是返回值是DrawResult类型。可以在CacheDrawScope接口声明中发现仅有onDrawBehind与onDrawWithContent这两个API提供了DrawResult类型返回值,实际上这两个API和前面所提及的drawBehind与drawWithContent用法是完全相同的。源码如下:
fun Modifier.drawWithCache(
onBuildDrawCache: CacheDrawScope.() -> DrawResult //CacheDrawScope
) = composed(
inspectorInfo = debugInspectorInfo {
name = "drawWithCache"
properties["onBuildDrawCache"] = onBuildDrawCache
}
) {
val cacheDrawScope = remember { CacheDrawScope() }
this.then(DrawContentCacheModifier(cacheDrawScope, onBuildDrawCache))
}
//CacheDrawScope
class CacheDrawScope internal constructor() : Density {
...
fun onDrawBehind(block: DrawScope.() -> Unit): DrawResult = onDrawWithContent {
block()
drawContent()
}
fun onDrawWithContent(block: ContentDrawScope.() -> Unit): DrawResult {
return DrawResult(block).also { drawResult = it }
}
...
这里使用drawCache来绘制多张图片,并不断改变这些图片的透明度。假设每张图片像素尺寸都比较大,一次性把这些图片全部装载到内存不仅耗时,并且也会占用大量内存空间。每当透明度发生变化时,我们不希望重新加载这些图片。在这个场景下,只需使用drawWithCache方法,将图片加载过程放到缓存区中完成就可以了。
由于我们暂时还没有学习动画相关知识,这里大家可以简单理解为利用transition创建了个无限循环变化的alpha透明度状态。接下来就可以在drawWithCache缓存区域中加载ImageBitmap实例了,并在DrawScope中使用这些ImageBitmap实例与前面声明的无限循环变化的alpha透明度状态。
@Composable
fun DrawCache() {
//获取上下文
val context = LocalContext.current
//动态改变alpha
val transition = rememberInfiniteTransition(label = "")
val alpha by transition.animateFloat(
initialValue = 0F,
targetValue = 1F,
animationSpec = infiniteRepeatable( //无限重复
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
), label = ""
)
Box(modifier = Modifier
.size(300.dp)
.drawWithCache { //drawWithCache
val imageBitmap =
ImageBitmap.imageResource(context.resources, R.mipmap.rabit2) //图片资源
onDrawBehind { //绘制背景
drawImage(
image = imageBitmap,
dstSize = IntSize(
width = 300.dp.roundToPx(),
height = 300.dp.roundToPx()
),
dstOffset = IntOffset.Zero,
alpha = alpha
)
}
}) {}
}
UI效果
参考内容
本文为学习博客,内容来自书籍《Jetpack Compose 从入门到实战》,代码为具体实践。
- 上一篇:hashCode与equals深度剖析与源码详解
- 下一篇:如何让谷歌字体隐私友好
相关推荐
- 如何从Jetpack订阅切换到邮件Chimp、aweber等
- Jetpack Compose Modifier2——LayoutModifier
- Dart 启动流程解析:探秘梦之起源
- 鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 鸿蒙HarmonyOS实战-ArkUI组件(Flex)
- 鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 鸿蒙HarmonyOS实战-ArkUI组件(GridRow/GridCol)
- 鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 开启Android学习之旅-2-架构组件实现数据列表及添加(kotlin)
- 鸿蒙 Ark Ui UIAbility组件生命周期
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8Android广播如何解决Sending non-protected broadcast问题
- 9鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版