使用Jetpack Compose构建时间轴组件的逐步指南

news2024/9/24 21:24:35

使用Jetpack Compose构建时间轴组件的逐步指南

最近,我们开发一个时间轴组件,显示用户与客户之间的对话。每个对话节点应具有自己的颜色,取决于消息的状态,并且连接消息的线条形成颜色之间的渐变过渡。

我们慷慨地估计了未来的工作,并开始使用Compose来实现它。令人高兴的是,仅仅两个小时后,我们就拥有了一个完全功能的时间轴组件。因此,我们写了这篇文章,为其他开发者提供一些在使用Compose解决类似挑战时的灵感。

简而言之,本文将探讨以下内容:

  • 创建一个漂亮的时间轴组件,无需使用任何第三方库
  • 高级使用Modifier.drawBehind()在Composable内容后绘制到画布中
  • 测试Composable代码的性能,使用Compose编译器报告和布局检查器。

在深入探讨之前,让我们从Dribbble上的一些时间轴示例中获取一些灵感:

想象一下候选人与人力资源代表之间的对话。虽然已经完成了一些招聘阶段,但仍有未来的阶段要期待。 同时,当前阶段可能也需要您的注意或额外的操作。
这个时间轴实际上就是一列节点。因此,我们最初的重点将是解决如何绘制单个节点。

每个时间轴项目由一个表示时间轴中时刻的圆圈和一些内容(在这种情况下是一条消息)组成。我们希望这个内容是动态的,并且可以从外部传递为参数。因此,我们的时间轴节点不知道我们将在圆圈右侧展示什么内容。

@Composable
fun TimelineNode(
    content: @Composable BoxScope.(modifier: Modifier) -> Unit
) {
    Box(
        modifier = Modifier.wrapContentSize()
    ) {
        content(Modifier)
    }
}

为了可视化我们所写的内容,我们将创建一个小预览,其中包含三个节点的列。我们创建一个MessageBubble组合,并将其用作每个时间轴节点的内容。

@Composable
private fun MessageBubble(modifier: Modifier, containerColor: Color) {
    Card(
        modifier = modifier
            .width(200.dp)
            .height(100.dp),
        colors = CardDefaults.cardColors(containerColor = containerColor)
    ) {}
}
@Preview(showBackground = true)
@Composable
private fun TimelinePreview() {
    TimelineComposeComponentTheme {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            TimelineNode() { modifier -> MessageBubble(modifier, containerColor = LightBlue) }
            TimelineNode() { modifier -> MessageBubble(modifier, containerColor = Purple) }
            TimelineNode() { modifier -> MessageBubble(modifier, containerColor = Coral) }
        }
    }
}

好的,现在我们有了TimelineNode的列,但它们都紧密地排列在一起。我们需要添加一些间距。

步骤1:添加间距

根据设计,每个项目之间应有32dp的间距(我们将这个参数命名为spacerBetweenNodes)。另外,我们的内容应该与时间轴本身有16dp的偏移(contentStartOffset)。
spacerBetweenNodes和contentStartOffset参数示意图

此外,我们的节点外观取决于其位置。对于最后一个元素,我们不需要绘制线条或添加间距。为了处理这种情况,我们将定义一个枚举:

enum class TimelineNodePosition {
    FIRST,
    MIDDLE,
    LAST
}

我们将这些额外的参数添加到TimelineNode的签名中。之后,我们将所传递给内容lambda的modifier应用所需的填充,用于绘制内容。

@Composable
fun TimelineNode(
    // 1. we add new parameters here
    position: TimelineNodePosition,
    contentStartOffset: Dp = 16.dp,
    spacerBetweenNodes: Dp = 32.dp,
    content: @Composable BoxScope.(modifier: Modifier) -> Unit
) {
    Box(
        modifier = Modifier.wrapContentSize()
    ) {
        content(
            Modifier
                .padding(
                    // 2. we apply our paddings
                    start = contentStartOffset,
                    bottom = if (position != TimelineNodePosition.LAST) {
                        spacerBetweenNodes
                    } else {
                        0.dp
                    }
                )
        )
    }
}

TimelineNodePosition枚举实际上可以是一个布尔标志,你可能会注意到。是的,可以是布尔标志!如果你对它没有其他用途,可以自由地简化和调整代码以适应你的用例。

我们将相应地调整我们的预览:

@Preview(showBackground = true)
@Composable
private fun TimelinePreview() {
    AppTheme {
        Column(...) {
            TimelineNode(
                position = TimelineNodePosition.FIRST,
            ) { modifier -> MessageBubble(modifier, containerColor = LightBlue) }

            TimelineNode(
                position = TimelineNodePosition.MIDDLE,
            ) { modifier -> MessageBubble(modifier, containerColor = Purple) }

            TimelineNode(
                TimelineNodePosition.LAST
            ) { modifier -> MessageBubble(modifier, containerColor = Coral) }
        }
    }
}

通过这些更新,我们的时间轴元素现在有了正确的间距。

很好!接下来,我们要添加漂亮的圆圈,并在每个TimelineNode的背后绘制渐变线条。

步骤2:绘制圆圈

让我们首先定义一个描述我们要绘制的圆圈的类:

data class CircleParameters(
    val radius: Dp,
    val backgroundColor: Color
)

现在你想知道我们在Compose中需要用什么绘制在Canvas上。有一个修饰符,可以在我们的情况下帮助我们 - Modifier.drawBehind

Modifier.drawBehind允许你在屏幕上绘制Composable内容背后的DrawScope操作。

你可以在这个页面上关于使用绘制修饰符的内容:

https://developer.android.com/jetpack/compose/graphics/draw/modifiers

为了在我们的画布的左上角创建一个圆圈,我们将使用drawCircle()函数:

@Composable
fun TimelineNode(
    // 1. we add a new parameter here
    circleParameters: CircleParameters,
    ...
) {
    Box(
        modifier = Modifier
            .wrapContentSize()
            .drawBehind {
                // 2. draw a circle here ->
                val circleRadiusInPx = circleParameters.radius.toPx()
                drawCircle(
                    color = circleParameters.backgroundColor,
                    radius = circleRadiusInPx,
                    center = Offset(circleRadiusInPx, circleRadiusInPx)
                )
            }
    ) {
        content(...)
    }
}

现在,我们的时间轴画布上有了漂亮的圆圈!

步骤3:绘制线条

接下来,我们创建一个类来定义线条的外观:

data class LineParameters(
    val strokeWidth: Dp,
    val brush: Brush
)

现在是时候将我们的圆圈与线条连接起来。我们不需要为最后一个元素绘制线条,因此我们将LineParameters定义为可为空。我们的线条从圆圈底部到当前项目的底部。

.drawBehind {
    val circleRadiusInPx = circleParameters.radius.toPx()
    drawCircle(...)
    // we added drawing a line here ->
    lineParameters?.let{
        drawLine(
            brush = lineParameters.brush,
            start = Offset(x = circleRadiusInPx, y = circleRadiusInPx * 2),
            end = Offset(x = circleRadiusInPx, y = this.size.height),
            strokeWidth = lineParameters.strokeWidth.toPx()
        )
    
}

为了欣赏我们的工作,我们应该在预览中提供所需的LineParameters。作为懒惰的开发者,我们不想一遍又一遍地创建渐变刷子,所以我们引入了一个实用对象:

object LineParametersDefaults {

    private val defaultStrokeWidth = 3.dp

    fun linearGradient(
        strokeWidth: Dp = defaultLinearGradient,
        startColor: Color,
        endColor: Color,
        startY: Float = 0.0f,
        endY: Float = Float.POSITIVE_INFINITY
    ): LineParameters {
        val brush = Brush.verticalGradient(
            colors = listOf(startColor, endColor),
            startY = startY,
            endY = endY
        )
        return LineParameters(strokeWidth, brush)
    }
}

即使对于圆圈的创建,我们尽管还没有很多用于自定义圆圈的参数,也要做同样的操作:

object CircleParametersDefaults {

    private val defaultCircleRadius = 12.dp

    fun circleParameters(
        radius: Dp = defaultCircleRadius,
        backgroundColor: Color = Cyan
    ) = CircleParameters(radius, backgroundColor)
}

准备好这些实用对象后,让我们更新我们的预览:

@Preview(showBackground = true)
@Composable
private fun TimelinePreview() {
    TimelineComposeComponentTheme {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            TimelineNode(
                position = TimelineNodePosition.FIRST,
                circleParameters = CircleParametersDefaults.circleParameters(
                    backgroundColor = LightBlue
                ),
                lineParameters = LineParametersDefaults.linearGradient(
                    startColor = LightBlue,
                    endColor = Purple
                ),
            ) { modifier -> MessageBubble(modifier, containerColor = LightBlue) }

            TimelineNode(
                position = TimelineNodePosition.MIDDLE,
                circleParameters = CircleParametersDefaults.circleParameters(
                    backgroundColor = Purple
                ),
                lineParameters = LineParametersDefaults.linearGradient(
                    startColor = Purple,
                    endColor = Coral
                ),
            ) { modifier -> MessageBubble(modifier, containerColor = Purple) }

            TimelineNode(
                TimelineNodePosition.LAST,
                circleParameters = CircleParametersDefaults.circleParameters(
                    backgroundColor = Coral
                ),
            ) { modifier -> MessageBubble(modifier, containerColor = Coral) }
        }
    }
}

现在,我们可以欣赏时间轴元素之间的丰富多彩的渐变。

(可选步骤):疯狂添加额外的装饰

根据您的设计,您可能希望添加图标、描边或其他您可以在画布上绘制的内容。TimelineNode的完整版本具有扩展功能集,可以在GitHub上找到示例。

https://github.com/VitaSokolova/TimelineComposeComponent/blob/master/app/src/main/java/vita/sokolova/timeline/TimelineNode.kt

在我们的预览中,我们手动在列中创建了“TimelineNode”,但您也可以在LazyColumn中使用TimelineNode,并根据消息的状态动态填充所有颜色参数。

使用Compose编译器报告检查稳定性

在UI性能方面,您可能经常会遇到意外的性能下降,这是由于您没有预料到的多余的重组周期造成的。许多非平凡的错误可能导致这种行为。

因此,现在是时候检查我们的Compose可组合是否表现良好。为此,我们首先将使用Compose编译器报告。

要在您的项目中启用Compose编译器报告,请查看本文:

https://developer.android.com/studio/preview/features#compose-compiler-reports

为了调试您的可组合性能稳定性,我们运行以下Gradle任务:

./gradlew assembleRelease -PcomposeCompilerReports=true

它将在您的模块 -> build -> compose_compiler目录中生成三个输出文件:

首先,让我们检查我们可组合中使用的数据模型的稳定性。我们转到app_release-classes.txt

stable class CircleParameters {
  stable val radius: Dp
  stable val backgroundColor: Color
  stable val stroke: StrokeParameters?
  stable val icon: Int?
  <runtime stability> = Stable
}
stable class LineParameters {
  stable val strokeWidth: Dp
  stable val brush: Brush
  <runtime stability> = Stable
}

非常好!我们在可组合中用作输入参数的所有类都标记为稳定。这是一个非常好的标志,这意味着Compose编译器将了解此类的内容何时发生变化,并仅在必要时触发重组。

接下来,我们检查app_release-composables.txt

restartable skippable scheme("[androidx.compose.ui.UiComposable, [androidx.compose.ui.UiComposable]]") fun TimelineNode(
  stable position: TimelineNodePosition
  stable circleParameters: CircleParameters
  stable lineParameters: LineParameters? = @static null
  stable contentStartOffset: Dp
  stable spacer: Dp
  stable content: @[ExtensionFunctionType] Function4<BoxScope, @[ParameterName(name = 'modifier')] Modifier, Composer, Int, Unit>
)

我们的TimelineNode组合是完全可重启、可跳过和稳定的(因为所有输入参数都是稳定的)。这意味着,Compose将仅在输入参数中的内容真正发生变化时触发重组。

使用布局检查器检查重组次数

但是我们是不是有点过度担心了?是的,我们是!让我们在布局检查器中运行它,并确保我们没有任何无限循环重组。不要忘记在布局检查器设置中启用“显示重组计数”。

我们添加了一些虚拟数据来显示在我们的时间轴上,并使用LazyColumn来呈现这些动态数据。
No recompositions happen on static list of elements

如果我们只是打开我们的应用程序,我们不会看到任何重组发生,这很好。但是让我们对其进行一些压力测试。我们添加了一个浮动操作按钮,该按钮会在LazyColumn的开头添加新消息。

每次添加新节点时,我们会看到LazyColumn元素的重组,这是预期的。但是,我们还可以看到,对于某些元素,重组被跳过了,因为它们的内容没有发生变化。这正是我们总是想要实现的,这意味着我们的性能已经足够好了。

结论

我们的工作完成了,我们有了一个漂亮的Compose组件来显示时间轴。它可以从Compose编译器的角度进行自定义和稳定。

GitHub

https://github.com/VitaSokolova/TimelineComposeComponent

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++学习day--18 空指针和函数指针、引用

1、void 类型指针 void > 空类型 void* > 空类型指针&#xff0c; 只存储地址的值&#xff0c;丢失类型&#xff0c;无法访问&#xff0c;要访问其值&#xff0c;我们必须对这个指 针做出正确的类型转换&#xff0c;然后再间接引用指针 。 所有其它类型的指针都可以隐…

基于C语言 --- 自己写一个扫雷小游戏

C语言程序设计笔记---020 初阶扫雷小游戏(开源)1、arr_main2.c程序大纲2、arr_game2.h3、arr_game2.c3.1、 自定义初化函数 InitBoard( ) 和 自定义显示函数 DisPlayBoard( )3.2、 自定义布置雷函数 SetMine( )3.4、 自定义排查雷函数 FindMine( ) 4、结束语 初阶扫雷小游戏(开…

Redis安装部署(基于windows平台)

redis简介 键值对存储数据库是NoSQL数据库的一种类型&#xff0c;也是最简单的NoSQL数据库。顾名思义&#xff0c;键值对存储数据库中的数据是以键值对的形式来存储的。常见的键值对存储数据库有Redis、Tokyo Cabinet/Tyrant、Voldemort以及Oracle BDB数据库。 Remote Diction…

1.4 信息安全管理

数据参考&#xff1a;CISP官方 目录 信息安全管理基础信息安全管理体系信息安全管理实践 一、信息安全管理基础 1、信息 信息是一种资产&#xff0c;与其他重要的业务资产一样&#xff0c;对组织业务必不可少&#xff0c;因此需要得到适当的保护。 2、信息的价值 企业…

数据结构和算法入门(时间/空间复杂度介绍--java版)

数据结构和算法入门&#xff08;时间/空间复杂度介绍–java版&#xff09; write in front 作者&#xff1a; 向大佬学习 专栏&#xff1a; 数据结构&#xff08;java版&#xff09; 作者简介&#xff1a;大二学生 希望能学习其同学和大佬的经验&#xff01; 本篇博客简介&…

定时任务调度 xxl-job

框架地址 https://gitee.com/jiaruiguo/xxl-job.git项目说明 调度管理系统 xxl-job-admin 定时任务实现系统 普通系统&#xff1a; xxl-job-executor-sample-frameless 微服务系统&#xff1a;xxl-job-executor-sample-springboot 配置说明 工程名&#xff1a;xxl-job-execut…

中国农村程序员学习此【JavaScript教程】购买大平层,开上帕拉梅拉,迎娶白富美出任CEO走上人生巅峰

注&#xff1a;最后有面试挑战&#xff0c;看看自己掌握了吗 文章目录 在 Switch 语句添加多个相同选项从函数返回布尔值--聪明方法undefined创建 JavaScript 对象通过点号表示法访问对象属性使用方括号表示法访问对象属性通过变量访问对象属性给 JavaScript 对象添加新属性删除…

AD21 PCB设计的高级应用(八)Draftsman的应用

&#xff08;八&#xff09;Draftsman的应用 1.创建Draftsman文档2.Draftsman页面选项设置3.放置绘图数据3.1 装配图3.2 板制造图3.3 钻孔图和钻孔列表3.4 图层堆栈图例3.5 BOM3.6 标注、注释、测量尺寸 4.文档输出4.1 打印或者导出为PDF4.2 添加到Output job Draftsman 是为电…

windows基础命令

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一.目录和文件的操作 1.cd 命令 切换到d盘 2.目录分为相对路径和绝对路径 3. dir命令 用于显示目录和文件列表 4. md 或 mkdir 创建目录 5. rd 用于删…

【编程语言 · C语言 · 共用体】

【编程语言 C语言 共用体】https://mp.weixin.qq.com/s?__bizMzg4NTE5MDAzOA&mid2247491502&idx1&snd531f724641b18619225de4bbcd02998&chksmcfade357f8da6a41f514ba72d817cc029f8f2a89d3753bfe5c547801abb3d2e080554e67d677&payreadticketHJqAIlk_6GWs…

Uncaught SyntaxError: ‘‘ string literal contains an unescaped line break

今天在修改前端页面的时候&#xff0c;页面报错了&#xff0c;提示了这个信息 Uncaught SyntaxError: string literal contains an unescaped line break 问题指向这行代码&#xff0c;这就是通过JS渲染一个easyui的搜索框&#xff0c;仔细确认之后&#xff0c;发现没有任何问…

Go学习第一天

闲聊两句 从事java后端开发8年多&#xff0c;期间也曾零星看过Go语言、Python、Erlang等等&#xff0c;但都未曾认真学习过&#xff0c;恰好公司最近项目需要&#xff0c;之前用Go开发的项目因为同事离职&#xff0c;暂未人来接手&#xff0c;所以老大就找到我和另外一个同事&…

算法自学__背包动态规划

例1 P5020 [NOIP2018 提高组] 货币系统 题目描述 在网友的国度中共有 n n n 种不同面额的货币&#xff0c;第 i i i 种货币的面额为 a [ i ] a[i] a[i]&#xff0c;你可以假设每一种货币都有无穷多张。为了方便&#xff0c;我们把货币种数为 n n n、面额数组为 a [ 1.. …

unity 使用Vuforia扫描物体( ModelTarget 模型目标)

1、下载vuforia插件vufora 2、下载模型生成器Model Target Generator 3、将vuforia插件导入到unity &#xff0c;我使用的unity是2021版本&#xff0c;导出插件时&#xff0c;只显示有两个文件&#xff0c;导入后&#xff0c;会有一个弹框 让更新插件&#xff0c;点击updata&am…

【编程语言 · C语言 · calloc和realloc】

【编程语言 C语言 calloc和realloc】https://mp.weixin.qq.com/s?__bizMzg4NTE5MDAzOA&mid2247491544&idx1&sn72d8f9931cfa7ce7441a3248475ab619&chksmcfade321f8da6a374a5935bb46441a03a007c0589db6b8afa8c1991854d632a3201553e37b0b&payreadticketHGy…

[算法很美打卡第四天] 字符串篇(中)

文章目录 压缩字符串代码 判断两字符串的字符集是否相同代码 旋转词代码 反转单词代码 回文串验证代码 去掉字符串中连接出现的k次的0代码 压缩字符串 代码 package 每日算法学习打卡.算法打卡.八月份;public class test1 {public static void main(String[] args) {String s …

替换linux的文泉驿正黑fonts-wqy-zenhei字体 替换linux默认中文字体

WSL 怎么替换 linux 的文泉驿正黑 fonts-wqy-zenhei 字体 WSL 怎么替换 linux 默认中文字体 在 wsl 中默认是没有 gnome 界面或者 xface 的&#xff0c;但是我需要使用 wsl 开发 electron 或者使用 chrome 浏览器。这个时候系统就会调用默认的系统字体了。 我使用的是 debian…

国产分布式数据库——TDSQL性能分析工具

一、TDSQL概述 TDSQL是腾讯研发的一款兼容MySQL协议的国产分布式数据库&#xff0c;适用于大并发、高性能、大容量的OLTP类场景。TDSQL分为集中式和分布式版本&#xff0c;分布式版可支持分布式事务&#xff0c;但性能不如单机事务&#xff0c;性能会有一定的损耗&#xff0c;…

Android系统的进程管理(创建->优先级->回收)

一、进程的创建 1、概述 Android系统以Linux内核为基础&#xff0c;所以对于进程的管理自然离不开Linux本身提供的机制。例如&#xff1a; 通过fork来创建进行通过信号量来管理进程通过proc文件系统来查询和调整进程状态 等 对于Android来说&#xff0c;进程管理的主要内容…

EMC VNX1系列存储电池状态说明

SPS电池正常的状态为“Present”。 SPS电池故障时的状态为“Faulted”。 更换SPS后&#xff0c;SPS开始充电&#xff0c;此时状态显示为“Not Ready”状态。 充电完成后显示为Present状态。如果充电完成后状态前面有“F”标记&#xff0c;则需要重启对应的控制器以更新SPS…