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

1,541 阅读5分钟

到目前为止,我们了解了延迟列表和延迟网格的工作原理,但是,如果你需要进行自定义,该怎么办呢?

LazyLayout

在RecyclerView中,你可以实现自己的布局管理器。为了在Compose 1.2中达到同样的目标。一个叫做LazyLayout的API.

延迟列表和延迟网格基于LazyLayout API构建.

如果你需要针对自定义用例实现自己的延迟布局,可以考虑为某个现有实现创建分支。

image.png

例如,Compose for Wear OS有一个自定义的Wear专用延迟列表,可根据与组件中心的距离缩放项。它是一个很好的LazyLayout用例。

我们会在内部使用LazyLayout API来构建交错网格。在交错网格中,项在垂直方向可以有不同的高度。

image.png

Item animations

呼声最高的功能之一是为列表项变化添加动画效果。所以在Compose1.1中,针对延迟列表推出了项重新排序动画功能

ezgif.com-video-to-gif.gif

Compose1.2中,将这项功能移植到延迟网格。

ezgif.com-video-to-gif (1).gif

这个API非常简单,你只需为项内容设置animateItemPalcement修饰符,你甚至可以根据需要设定自定义动画规格。

LazyColumn {
    items(books, key = { it.id }) {
        Row(Modifier.animateItemPlacement(
            tween(durationMillis = 250)
        )) {
            ...
        }
    }
}

一定要为项提供键值以便找到被移动元素的新位置

image.png

除了支持动画之外,通过提供键,还能够正确处理重新排序。例如,当项的位置发生变化时,将项和组合项中的记忆状态随相应项一起移动

LazyColumn {
    items(books, key = { it.id }) {
        val remembereddValue = remember {
            Random.nextInt()
        }
    }
}

不过对于可用作项键的类型有一条限制,键的类型应受Bundle支持,这是Android的机制。其作用是在创建activity时保留相应状态。Bundle支持基元、枚举或Parcelable等类型。

LazyColumn {
    items(books, key= {
        // primitives, enums, Parcelable, etc
    }) {
        ...
    }
}

键必须受Bundle支持以便在创建activity时,甚至在你滚动离开项,然后滚动回来时,这个项可组合项中rememberSaveable仍可以恢复。

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

注意项

看看如何充分利用延迟布局

第一条,不要使用大小为0像素的项

在某些情况下你可能会这样做。

例如,当你希望在后期阶段异步检索图片之类的数据来填充列表项时。

image.png

为了说明为什么要避免这样做。我简单介绍一下延迟布局对子项的处理方式,相同的原则适用于所有延迟列表和延迟网格。

但我们举一个具体例子:

LazyColumn在首次测量项时会认为项的高度没有限制。

image.png

``` val childConstraints = Constraints( maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity, maxHeight = if (!isVertical) constraints.maxHeight else Constraints.Infinity ) ```

也就是说,它不会限制项排列。而会一直组合项,直到使用计算的测量高度填满可用的视图之后,列表会停止组合子项。这个模式支持按需添加和移出内容。延迟布局也正是基于这一概念。

image.png

如果列表项的初始高度为0像素,则意味着在首次测量时LazyColumn会组合它的所有项,因为项的高度为0像素。这些项能轻松地容纳到窗口中。

@Composable
fun Item() {
    Image(
        painter = rememberImagePainter(
            data = imageUrl
        ),
    ...
    )
}

image.png

几毫秒后,图片加载,项重组并开始显示图片。这时延迟列表意识到实际只有一些项能容纳到窗口中。所以会舍弃首次测量时不必要地组合起来的其余项。

image.png

为避免这种情况,你应该为项设置默认大小,以便延迟布局正确计算实际上有多少项可以容纳到窗口中。

@Composable
fun Item() {
    Image(
        painter = rememberImagePainter(
            data = imageUrl
        ),
        modifier = Modifier.size(30.dp),
        ...
    )
}

image.png

如果你知道自己的项在数据异步加载后的大致大小,最好的做法是确保加载前后项的大小保持不变。例如通过添加一些占位符达到这个目的的

image.png

第二条,避免嵌套可向同一方向滚动的组件

准确来说,这个提示仅适用于一种情况,也就是将没有预定义尺寸的可滚动子级嵌套在可向同一方向滚动的另一个父级中。例如,尝试在可垂直滚动的父级Column中嵌套高度不固定的子级LazyColumn.

// Throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        ...
    }
}

image.png

实际上即使你尝试这样做,也无法做到,Compose不支持这种嵌套.这可能听起来凌然不解,因为使用View,你可以在ScrollView内封装RecyclerView,但这是有代价的,性能会受到严重影响原因和我们在前面所说的,大小为0像素的项那种情况类似。ScrollView在测量子项时,会认为子项的高度可以为任意值,这样,你的RecyclerView就能够不受限制。继而能够立即创建所有项导致无法循环使用。所以Compose会尝试引导你避免采用这种模式以防止出现这个问题。

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    ...
    <androidx.recyclerView.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        ... />
    ...
</ScrollView>

而只需将所有可组合项封装在一个父级LazyColumn内,并使用强大的DSL传入不同类型的内容就能实现相同的结果。这样系统就可以在一个位置,既发出标题等单个列表项,又发出多个列表项

LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        Item(item)
    }
    item {
        Footer()
    }
}

image.png

但请注意,嵌套不同方向布局的情形是允许的。例如在可滚动的父级Row中嵌套子级LazyColumn

val scrollState = rememberScrollState()

Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        ...
    }
}

image.png

还有一种情形也是允许的,你还是使用相同方向的布局,但同时为嵌套的子级设置固定尺寸

val scrollState = rememberScrollState()

Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        ...
    }
}

image.png