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

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

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

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

相关文章

北方多地暴雨引思考:如何降低暴雨负面影响?

受今年第五号台风“杜苏芮”残余环流北上影响&#xff0c;北方多地这两天出现了大范围的强降雨。 7月31日晚上&#xff0c;国家防总办公室、应急管理部加密研判会商&#xff0c;与中国气象局、水利部会商研判&#xff0c;视频连线北京、天津、河北等重点省份&#xff0c;滚动分…

Kafka3.0.0版本——Broker(总体工作流程)

目录 一、Kafka中Broker总体工作流程图解二、Kafka中Broker总体工作流程步骤解析 一、Kafka中Broker总体工作流程图解 总体工作流程图解 二、Kafka中Broker总体工作流程步骤解析 1、broker启动后在zk中注册&#xff0c;如下图所示&#xff1a; 2、controller谁先注册&…

STM32 CubeMX 定时器(普通模式和PWM模式)

STM32 CubeMX STM32 CubeMX 定时器&#xff08;普通模式和PWM模式&#xff09; STM32 CubeMXSTM32 CubeMX 普通模式一、STM32 CubeMX 设置二、代码部分STM32 CubeMX PWM模式一、STM32 CubeMX 设置二、代码部分总结 STM32 CubeMX 普通模式 一、STM32 CubeMX 设置 二、代码部分 …

《工具箱-VNCServer》配置VNCServer,使用VNCViewer实现局域网内页面共享

VNCServer设置 通过VNCServer配置&#xff0c;与VNCviewer配套使用 1.下载并安装VNCServer 2.邮箱密码注册后用户登录 3.设置VNC密码 4.设置viewer不能控制本机 5.打开VNClicensewiz&#xff0c;选择“Enter a license key …” BQ24G-PDXE4-KKKRS-WBHZE-F5RCA BQ24G-PDXE4-…

详解AMQP协议以及JAVA体系中的AMQP

目录 1.概述 1.1.简介 1.2.抽象模型 2.spring中的amqp 2.1.spring amqp 2.2.spring boot amqp 1.概述 1.1.简介 AMQP&#xff0c;Advanced Message Queuing Protocol&#xff0c;高级消息队列协议。 百度百科上的介绍&#xff1a; 一个提供统一消息服务的应用层标准高…

FFmpeg 音视频开发工具

目录 FFmpeg 下载与安装 ffmpeg 使用快速入门 ffplay 使用快速入门 FFmpeg 全套下载与安装 1、FFmpeg 是处理音频、视频、字幕和相关元数据等多媒体内容的库和工具的集合。一个完整的跨平台解决方案&#xff0c;用于录制、转换和流式传输音频和视频。 官网&#xff1a;http…

力扣 343. 整数拆分

题目来源&#xff1a;https://leetcode.cn/problems/integer-break/description/ C题解1&#xff1a;动态规划。dp[i] 代表数字i拆分后得到的最大乘积。递归公式为拆分后两个数的最大乘积相乘&#xff0c;即 dp[i] max(dp[i], dp[j] * dp[i-j])。对于n2或3需要另外讨论。 cla…

Android 面试题 线程间通信 六

&#x1f525; 主线程向子线程发送消息 Threadhandler&#x1f525; 子线程中定义Handler&#xff0c;Handler定义在哪个线程中&#xff0c;就跟那个线程绑定&#xff0c;在线程中绑定Handler需要调用Looper.prepare(); 方法&#xff0c;主线程中不调用是因为主线程默认帮你调用…

IP 工具

什么是IP 工具 IP 工具是用于轻松扫描和排除网络 IP 地址空间故障的网络工程工具。IP 工具使网络管理员能够审核、跟踪和监视 IP 地址、子网以及使用 IP 的设备和主机的性能。这个全面的网络工程工具集包括高级 IP 工具&#xff0c;如 Ping、系统资源管理器、MAC 地址解析器和…

网格简化(QEM)学习笔记

文章目录 网格简化(QEM)1 概述与原理1.1 网格简化的应用1.2 常见的简化操作1.3 二次误差度量 2 算法流程2.1 逐步分析 3 Python代码实现3.1 测试结果 4 总结参考 网格简化(QEM) 1 概述与原理 网格简化&#xff0c;通过减少复杂网格数据的顶点、边和面的数量简化模型的表达&am…

Java版工程行业管理系统源码-专业的工程管理软件- 工程项目各模块及其功能点清单

&#xfeff; 工程项目管理软件&#xff08;工程项目管理系统&#xff09;对建设工程项目管理组织建设、项目策划决策、规划设计、施工建设到竣工交付、总结评估、运维运营&#xff0c;全过程、全方位的对项目进行综合管理 工程项目各模块及其功能点清单 一、系统管理 1、数据…

021 - STM32学习笔记 - Fatfs文件系统(三) - 细化与总结

021 - STM32学习笔记 - Fatfs文件系统&#xff08;三&#xff09; - 细化与总结 上节内容中&#xff0c;初步实现了FatFs文件系统的移植&#xff0c;并且实现了设备的挂载、文件打开/关闭与读写功能&#xff0c;这里对上节遗留的一些问题进行总结&#xff0c;并且继续完善文件…

Mybatis插件

文章目录 1. 如何自定义插件1.1 创建接口Interceptor的实现类1.2 配置拦截器1.3 运行程序 2. 插件原理2.1 解析过程2.2 创建代理对象2.2.1 Executor2.2.2 StatementHandler2.2. 3ParameterHandler2.2.4 ResultSetHandler 2.3 执行流程2.4 多拦截器的执行顺序 3. 1. 如何自定义插…

【Redis】内存数据库Redis进阶(Redis持久化)

目录 分布式缓存 Redis 四大问题Redis 持久化RDB (Redis DataBase)RDB执行时机RDB启动方式——save指令save指令相关配置save指令工作原理save配置自动执行 RDB启动方式——bgsave指令bgsave指令相关配置bgsave指令工作原理 RDB三种启动方式对比RDB特殊启动形式RDB优点与缺点 A…

Git全栈体系(三)

第六章 GitHub 操作 一、创建远程仓库 二、远程仓库操作 命令名称作用git remote -v查看当前所有远程地址别名git remote add 别名 远程地址起别名git push 别名 分支推送本地分支上的内容到远程仓库git clone 远程地址将远程仓库的内容克隆到本地git pull 远程库地址别名 远…

基于SpringCloud+Vue的分布式架构网上商城系统设计与实现(源码+LW+部署文档等)

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

Spring入门-技术简介、IOC技术、Bean、DI

前言 Spring是一个开源的项目&#xff0c;并不是单单的一个技术&#xff0c;发展至今已形成一种开发生态圈。也就是说我们可以完全使用Spring技术完成整个项目的构建、设计与开发。Spring是一个基于IOC和AOP的架构多层j2ee系统的架构。 SpringFramework&#xff1a;Spring框架…

06-向量的更多术语和表示法

向量 引入的概念&#xff1a;向量就是一组有序的数字, 我们在理解它的时候&#xff0c; 可以把它理解成是一个有效的线段&#xff0c;也可以把它理解成是空间中的一个点&#xff0c;那么与之相对应的一个数字&#xff0c;也就是我们在初等数学中学的一个一个数&#xff0c;我们…

GRNN神经网络原理与matlab实现

1案例背景 1.1GRNN神经网络概述 广义回归神经网络(GRNN Generalized Regression Neural Network&#xff09;是美国学者 Don-ald F. Specht在1991年提出的,它是径向基神经网络的一种。GRNN具有很强的非线性映射能力和柔性网络结构以及高度的容错性和鲁棒性,适用于解决非线性问…

关于综合能源智慧管理系统的架构及模式规划的研究

安科瑞 华楠 摘 要&#xff1a;探讨了国内外能源互联网的研究发展&#xff0c;分析了有关综合智慧能源管理系统的定位&#xff0c;以及系统的主要特点&#xff0c;研究了综合智慧能源管理系统的构架以及模式规划。 关键词&#xff1a;综合能源&#xff1b;智慧管理系统&#…