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

news/2025/2/12 10:11:14/

使用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 hereposition: 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 paddingsstart = 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 herecircleParameters: 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.dpfun 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.dpfun 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: Dpstable val backgroundColor: Colorstable val stroke: StrokeParameters?stable val icon: Int?<runtime stability> = Stable
}
stable class LineParameters {stable val strokeWidth: Dpstable 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: TimelineNodePositionstable circleParameters: CircleParametersstable lineParameters: LineParameters? = @static nullstable contentStartOffset: Dpstable spacer: Dpstable 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.ppmy.cn/news/997022.html

相关文章

【JS】中深拷贝浅拷贝的区别和实现方式

1、概念 赋值不属于拷贝JS的 深拷贝浅拷贝主要作用于多层数组或对象中&#xff0c;其中&#xff1a; 浅拷贝&#xff1a; 只对第一层 数组或对象进行复制&#xff0c;其它层复制的是原存储的内存地址&#xff08;修改其它深层级会影响原对象&#xff09; 深拷贝&#xff1a; …

时间计算:时间戳加减指定的分钟数__Niyyy_记

在开发中多多少少会遇到时间的计算&#xff0c;以下只是一个简单的例子。 将时间戳加减指定的分钟数&#xff0c;并将结果转换为年月日时分秒格式&#xff1a; function addMinutes(timestamp, minutes) {var date new Date(timestamp);date.setTime(date.getTime() minute…

系统架构设计师_备考第1天

文章目录 前言一、软考历史与体系二、考试价值与意义三、软考报名与交费四、考试介绍五、综合备考策略 前言 从今天开始&#xff0c;会认真备考系统架构设计师&#xff0c;希望95天后&#xff0c;拿下软考证书。 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可…

Rust基础入门【1】

“Why Rust”,installation & “Hello, World”,primitive types,string & &str,array & vector,mutability,enum,match,condition & statement Very brief history of Rust Rust main feature Dev. Env. primitive types sized?, string, vec mu…

python数学建模_1: 输入矩阵解决多供给地与多需求地的资源分配最优化问题

输入矩阵解决多供给地与多需求地的资源分配的线性最优化问题, 输入&#xff1a; 供给地的数目及其资源量&#xff0c;需求地的数目及其需求量&#xff0c;供给地的地理位置&#xff0c;需求地的地理位置 &#xff08;编写函数计算供给地和需求地的距离&#xff0c;按两点间距…

nodejs的字符串文字(‘‘)和模板文字(``)性能比较

nodejs的字符串文字(‘’)和模板文字()性能比较 js支持两种方式定义字符串&#xff1a; 使用 const str "Hello " "world!";使用 const worldText "world!" const str Hello ${worldText};我们可能不会太关注这些&#xff0c;应该都是怎…

ffmpeg和ffplay 推送和播放RTP流

ffmpeg 推送rtp流&#xff0c;保存成sdp文件&#xff0c;再用vlc播放sdp 下面这个是把abc.mp4文件推送到rtp://127.0.0.1:5004\abc.mp4端口,同时输出sdp文件到test_rtp_h264.sdp -an参数的作用是只推送视频, 没推送音频. ffmpeg -re -i d:\\abc\\abc.mp4 -an -f rtp rtp://12…