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

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

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

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

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

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

在深入探讨之前,让我们从Dribbble上的一些时间轴示例中获取一些灵感:
使用Jetpack Compose构建时间轴组件的逐步指南_第1张图片

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

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

@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) }
        }
    }
}

使用Jetpack Compose构建时间轴组件的逐步指南_第3张图片

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

步骤1:添加间距

根据设计,每个项目之间应有32dp的间距(我们将这个参数命名为spacerBetweenNodes)。另外,我们的内容应该与时间轴本身有16dp的偏移(contentStartOffset)。
使用Jetpack Compose构建时间轴组件的逐步指南_第4张图片

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

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) }
        }
    }
}

通过这些更新,我们的时间轴元素现在有了正确的间距。
使用Jetpack Compose构建时间轴组件的逐步指南_第5张图片
很好!接下来,我们要添加漂亮的圆圈,并在每个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(...)
    }
}

现在,我们的时间轴画布上有了漂亮的圆圈!
使用Jetpack Compose构建时间轴组件的逐步指南_第6张图片

步骤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) }
        }
    }
}

现在,我们可以欣赏时间轴元素之间的丰富多彩的渐变。
使用Jetpack Compose构建时间轴组件的逐步指南_第7张图片

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

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

https://github.com/VitaSokolova/TimelineComposeComponent/blob/master/app/src/main/java/vita/sokolova/timeline/TimelineNode.kt
使用Jetpack Compose构建时间轴组件的逐步指南_第8张图片

在我们的预览中,我们手动在列中创建了“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目录中生成三个输出文件:
使用Jetpack Compose构建时间轴组件的逐步指南_第9张图片

首先,让我们检查我们可组合中使用的数据模型的稳定性。我们转到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将仅在输入参数中的内容真正发生变化时触发重组。

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

但是我们是不是有点过度担心了?是的,我们是!让我们在布局检查器中运行它,并确保我们没有任何无限循环重组。不要忘记在布局检查器设置中启用“显示重组计数”。
使用Jetpack Compose构建时间轴组件的逐步指南_第10张图片

我们添加了一些虚拟数据来显示在我们的时间轴上,并使用LazyColumn来呈现这些动态数据。
使用Jetpack Compose构建时间轴组件的逐步指南_第11张图片

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

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

结论

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

GitHub

https://github.com/VitaSokolova/TimelineComposeComponent

你可能感兴趣的:(jetpack,compose,android,android,jetpack)