Jetpack Compose(第四趴)——Compose中的延迟布局(下)

1,808 阅读6分钟

建议

第一、注意将多个元素放入一个项中的情况

目前为止,我们已经了解简单的列表用法,在DSL lambda中为每个项发出了一个元素。

LazyVerticalGrid(
    ...
) {
    items(count) { index ->
        Item(index)
    }
}

image.png

但是由于没有编译时强制执行,你肯定想知道,如果发出多个元素,会怎么样?

LazyVerticalGrid(
    ...
) {
    Item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    ...
}

image.png

在这个示例中,第二个项lambda在一个代码块中发出两个项。好消息是,延迟布局能妥善地处理这种情况,它会逐个布置各个元素,就像它们是不同的项一样。

image.png

那为什么不应将所有元素放入一个项中呢?因为这样做会出现一些问题:

首先,当多个元素作为同一个项所含的部分发出时,系统会将它们作为一个实体进行处理。也就是说,这些元素无法再单独组合。如果一个元素在屏幕上可见与这个项对应的所有元素都必须得到组合的测量。正如你猜想的那样。如果过度使用,这可能会降低性能。

将所有元素放入同一个项属于极端情况,完全违背了使用延迟布局的初衷。除了潜在的性能问题外,将多个元素放入同一个项中还会干扰scrollToItemanimateScrollToItem。这是因为这些方法的索引参数会被解析为与从使用的DSL中派生的项数相关,而不是与实际的元素数相关。

image.png

在这个示例中,调用scrollToItem(2),实际上会使Item(3)显示在顶部.

不过,也有一些将多个元素放入同一个项的有效用例。例如在一个列表内添加多条分割线,分割线可以纳入前一个项中,就像这个示例展示的这样。我们不希望分割线更改滚动索引,所以不应该把分割线视为独立元素。另外由于分割线占的空间很小,所以不会影响到性能。分割线可能会在它之前的那个项可见时显示。

LazyVerticalGrid(
    ...
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    ...
}

image.png

第二、考虑使用自定义排列方式

通常,延迟列表包含许多项并且这些项所占的空间可能超过滚动容器的大小。不过,如果列表中填充的项很少。那么在设计中,你可以对这些项在窗口中的位置做出更具体的要求。务必在这种状态下测试你的列表,同时考虑你希望项如何显示。

例如:当没有足够的项来填充可用高度时,你可能需要在窗口底部显示一个页脚。为此我们可以使用自定义verticalArrangement,将它传递给LazyColumn

在下面,TopithFooter对象实现arrange方法,这样就为我们提供了totalSize,这是窗口的高度,还有sizes,也就是各个项的高度产生的偏移量,需要再outPositions数组中计算。

首先,我们将项逐个放在相应位置,然后,如果所用总高度低于窗口高度,我们就在底部放置页脚。可以看到,使用排列方式,你可以轻松地自定义延迟列表处理子项的方式满足自己的特定设计要求。

LazyColumn(
    modifier = Modifier.fillMaxHeigit(),
    verticalArrangement = TopWithFooter
) {
    ...
}

object TopWithFooter: Arrangement.Vertical {
    override fun Density.arrange(
        totalSize: Int,
        sizes: IntArray,
        outPositions: IntArray
    ) {
        var y = 0
        sizes.forEachIndexed { index, size ->
            outPositions[index] = y
            y += size
        }
        if (y < totalSize) {
            val lastIndex = outPositions.lastIndex
            outPositions[lastIndex] = totalSize - sizes.last()
        }
    }
}

image.png

性能

我们知道滚动性能非常重要。

首先需要注意的是,只有在发布模式下运行且启用R8优化时,你才能可靠地衡量延迟布局的性能。

buildTypes {
    release {
        minifyEnabled true
        proguardFiles
            getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

在调试build中,延迟布局滚广东可能会显得比较慢。

此外,如果你发现自己的应用在首次启动时很慢,之后又变快了。原因很可能是通过默认基准配置文件得到了修正。

关于这方面的详细介绍:Common Performance gotchas in Jectpack Compose

目前为了提高列表的滚动性能,做的一些优化

1、组合重复使用

第一项优化是组合重复使用,它与RecyclerView的功能有点类似。

我们来了解一下运作方式,滚动后,不再需要上一个可见项时,我们不会立即即将它丢弃,而是会保留一些这类项以便在需要组合新项时重复使用它们。

image.png

image.png

首先,由于不用执行丢弃项并启动全新组合所需的工作,我们能够节省一些时间。

其次,我们还尝试重复使用布局节点,它是界面树中每个布局的内部表示法,在重复使用的组合中组合新的项时,会尝试自动为具有类似界面结构的项重复使用内部布局节点对象。

实际上,如果发出的布局节点完全相同,对于尺寸和所有其他动态属性它们会具有相同的值,对这类组合,我们甚至可以跳过整个重新测量过程并且这全都是自动完成的。

不过有一件事只有在你的协助下才能完成,在Compose 1.2中,我们添加了一个新API,你可以利用它为列表中的每个项制定内容类型。

假设你要组合一个列表,其中包含两种不同类型的项

LazyColumn {
    items(elements, contentType = { it.type }) {
        ...
    }
}

在这种情况下,当你提供contentType时,Compose只能在相同类型之间重复使用组合。

image.png

前面说过,在组合具有像素结构的项时重复使用效率更高。提供contentType可以确保Compose不会尝试基于截然不同的B类型的项来组合A类型的项。因为那样会使重复使用组合的优势大打折扣。

预提取

实现的第二项性能优化成为“预提取”。同样,这与RecyclerView中的功能非常相似。

当在无需处理任何新项的情况下滚动时,LazyColumn可以毫无问题地在帧预算内完成所需步骤。

image.png

我们来了解一下主要步骤有哪些,可以看到,在界面线程上只需进行布局,以便应用新位置。然后,重新记录新的绘制命令;接着,绘制信息传输到渲染线程,而这会将命令传递给GPU。

但是,如果新项进入屏幕将需要完成更多步骤。因为我们首先需要组合新项内容,然后对它进行测量,当所有步骤所需的时间超出帧界面时,这类额外步骤会导致卡顿。

image.png

或许我们可以提前准备好这些项。可以看到,在前一个帧中,UI线程在完成所需的全部工作后就空闲了。我们可以将额外步骤放到这里完成。

image.png

这就是我们所说的预提取。这个帧中有可用的富余时间来预先组合,然后测量即将在屏幕上显示的项。这样我们就可以提前完成额外步骤,而不是让后续步骤等待该步骤完成。

image.png