了解如安在Compose中制作翻滚列表,以及这样为什么比运用RecyclerView更简单。了解为什么不允许嵌套翻滚列表、怎么选用不同办法完成嵌套、为什么列表项的大小决不能为0像素、为什么供给唯一的键十分重要,以及项动画怎么运作。最终,您将探索怎么显现网络、运用自定义布局管理器,以及了解怎么改善功用优化以进步翻滚速度。
一、推迟布局和Recycler的差异
假如你以前用过RecyclerView,那应该很熟悉推迟布局背后的主要概念:也便是在屏幕显现可翻滚的项列表时,分别烘托各个项,而非一次性烘托一切项。
假如你需求加载很多项或大型数据集,经过运用推迟布局,你能够按需增加内容,然后提高运用的功用和响应才能。
目前,在Compose 1.2中推迟布局包括
-
LazyColumn
,它是笔直翻滚列表 -
LazyRow
,它是水平翻滚列表 -
LazyGrids
,它供给这两种列表。
但View体系中运用的RecyclerView和Compose中运用推迟列表之间有一个很大的不同:差异就在于需求编写的代码量上。
RecyclerView
class FlowersAdapter(private val onClick: (Flower) -> Unit): ListAdapter<Flower, FlowersAdapter.FlowerViewHolder>(FlowerDiffCallback) {
class FlowerViewHolder(itemView: View, val onClick: (Flower) -> Unit): RecyclerView.ViewHolder(itemView) {
private val flowerTextView: TextView = itemView.findViewById(R.id.flower_text)
...
fun bind(flower: Flower) {
...
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlowerViewHolder {
...
}
override fun onBindViewHolder(holder: FlowerViewHolder, position: Int) {
...
}
}
这是你需求为RecyclerView适配器和ViewHolder编写的代码
这是RecyclerView的XML布局代码:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager" />
这是RecyclerView项布局的代码:
<LinearLayout
...>
<ImageView
android:id="@+id/flower_image"
android:layout_width="48dp"
android:layout_height="48dp"
... />
<TextView
android:id="@+id/flower_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
... />
</LinearLayout>
最终是将适配器绑定到RecyclerView所需的代码:
class FlowerListActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(saveInstanceState)
setContentView(R.layout.activity_main)
val flowersAdapter = FlowerAdapter { flower -> adapterOnClick(flower) }
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
recyclerView.adapter = flowersAdapter
...
}
}
这是在运用推迟列表时相同的工作量需求的代码:
@Composable
fun FlowerList(flowers: List<Flower>) {
LazyColumn {
item(flowers) { flower ->
FlowerItem(flower)
}
}
}
@Composable
fun FlowerItem(flower: Flower) {
Column {
Image(flower.image)
Text(flower.name)
}
}
它们完成的结果相同,但所需的代码更少,明显更易于读取和写入。
假如要在推迟列表中增加项,能够运用LazyListScope
DSL块来接受供给的内容,并将其显现为列表项
LazyColumn {
// LazyListScope block
item {
Text(header)
}
items(data) { item ->
Item(item)
}
}
这种办法与Compose中的其他常规布局略有不同,你能够只管描绘内容,推迟列表会处理其余一切工作。
这个API供给了两种刺进办法,你能够用包括一个项的块描绘一个项
或用包括多个项的块描绘多个项
也能够在同一列表中运用两者的组合来描绘内容。
假如你需求知道每个项的索引,例如,为了给偶数项和奇数项设置不同的颜色,你能够运用itemsIndexed
块来获取相应信息。
LazyColumn {
// LazyListScope block
item {
Text(header)
}
itemsIndexed(data) { index, item ->
Item(item, index)
}
}
二、自定义列表外观
2.1、contentPadding
增加项后,下一步是考虑怎么自定义列表的外观
运用推迟列表,这会十分简单。
例如,围绕列表内容增加一些内边距是一个常见的用例,假如你只想缩进整个列表,运用内边距修饰符能够轻松做到
LazyRow(
modifier = Modifier.padding(
start = 24.dp,
end = 24.dp
)
) {
items(data) {item ->
Item(item)
}
}
可是,这样在起浮榜首项和最终一项时,为了使内容保持在列表内边距的边界内就会裁剪内容。
假如你期望保存相同的内边距,一起仍然在列表的边界内翻滚内容而且还不裁剪,能够向列表的contentPadding
参数传递PaddingValues
,这样你就能够分别为每一侧设置相同的内边距
LazyRow(
contentPadding = PaddingValues(
start = 24.dp,
end = 24.dp
)
) {
items(data) { item ->
Item(item)
}
}
2.2、Arrangement.spaceBy
现在咱们来进一步优化内容的界面,默许状况下,你的列表会像这样黏在一起。
假如要在列表项之间增加一些规整的距离,你能够运用Arrangement.spaceBy
将它传递到列表
LazyRow(
horizontalArrangement = Arrangement.spaceBy(8.dp)
) {
items(data) { item ->
Item(item)
}
}
2.3、LazyListState
由于推迟列表旨在用于有很多内容或项要显现的状况,因此你有或许需求执行很多翻滚操作来浏览列表。所以需求关注的要害功用之一是怎么以编程办法观察和相应翻滚操作。
这里的要害是LazyListState
,它是一个重要的状况目标,可存储翻滚方位并包括关于你的列表的实用信息。
LazyColumn(
state = rememberLazyListState()
) {
items(data) { item ->
Item(item)
}
}
为了确保各个组合都会被记住你的状况,请运用rememberLazyListState
提高它并将它传递到列表
val state = rememberLazyListState()
LazeColumn(
state = state
) {
items(data) { item ->
Item(item)
}
}
有了LazyListState
,你能够获取榜首个可见项的索引和偏移量。这是相应翻滚时最常用的两个特点
val state = rememberLazyListState()
state.firseVisiableItemIndex
state.firstVisibleItemScrollOffset
例如,你能够根据榜首个可见项来确定是否显现用于翻滚到列表顶部的按钮
val state = rememberLazyListState()
val showScrollToTopButton by remember {
derivedStateOf {
state.firstVisibleItemIndex > 0
}
}
请注意,LazyListState特点常常变化,仅在一个组合内读取其特点会触发很多或许并无必要的重组。为防止这种状况,能够将它封装在remember
和derivedStateOf
中。这样能够确保只有当核算中运用的状况特点发生变化时才会进行重组。
LazyListState
还能够经过layoutInfo
目标供给其他有用信息,例如当前可见的项和总项数
state.layoutInfo.visibleItemsInfo
state.layoutInfo.totalItemCount
state.layoutInfo.visibleItemsInfo
.map { it.index }
举例来说,假如你期望在项彻底可见时播映其间的内容,在不彻底可见时暂停,能够运用这个目标来提取一切当前显现的项的索引,现在,咱们来看一个实例彻底完成“翻滚至顶部”按钮。
咱们能够运用LazyListState
因为它供给一个方便的挂起函数scrollToItem()
,让咱们能够快速回来或前往特定方位的某个项
val state = rememberLazyListState()
ScrollToTopButton(
onClick = {
// suspend function
state.scrollToItem(
index = 0
)
}
)
假如你喜欢更流畅的动画转场作用,能够运用animateScrollToItem
val state = rememberLazyListState()
ScrollToTopButton(
onClick = {
// suspend function
state.animateScrollToItem(
index = 0
)
}
)
请注意,它们都是挂起函数,所以需求从rememberCoroutineScope
供给的协程作用域中调用,然后,在按钮的onClick
参数内的协程作用域内启动翻滚函数
val state = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
ScrollToTopButton(
onClick = {
coroutineScope.launch {
state.animateScrollToItem(
index = 0
)
}
}
)
三、LazyGrid
Compose还供给了开箱即用的推迟网络,推迟网络最近经过从头规划。有了新的功用,API也在Compose 1.2中从实验阶段晋级为稳定版。
咱们来详细了解一下:你能够经过LazyVerticalGrid
和LazyHorizontalGrid
可组合项运用推迟网格。
LazyVerticalGrid
会在可笔直翻滚的容器中跨多个列显现所含的项,而LazyHorizontalGrid
则会在水平轴上有相同的行为。
网格的用法十分简单,与LazyColumn的用法根本相同。只不过增加了对笔直网格列的描绘。在这里,咱们拟定网格应该固定有两列。
LazyVerticalGrid(
columns = GridCells.Fiexd(2)
) {
items(data) { item ->
Item(item)
}
}
运用Compose中的网格与运用列表十分相似。列表与网格具有相同强大的API功用而且它们还运用十分相似的DSL来描绘内容。
例如,假如要在项之间增加距离,咱们只需运用spacedBy
摆放办法,这是相同之处。不同之处在于网格一起具有笔直和水平摆放办法。
LazyVerticalGrid(
colomn = GridCells.Fixed(2),
verticalArrangement = Arrangement.spaceBy(16.dp),
horizontalArrangement = Arrangement.spaceBy(16.dp)
) {
items(data) { item ->
Item(item)
}
}
网格会接受contentPadding
和lazyGridState
,后者具有与适用于列表的LazyListState
一样实用的功用。LazyGridState
会公开例如firstVisibleItemIndex
和layoutInfo
等信息。这是为了便于了解网格的当前布局状况。关于翻滚,它也供给相同的API,即scrollToItem
和animateScrollToItem
。
LazyVerticalGrid(
contentPadding = PaddingValues(...),
state = state // LazyGridState
...
)
state.firstVisibleItemIndex // Int
state.layoutInfo// LazyGridLayoutInfo
state.scrollToItem(...)
state.animateScrollToItem(...)
LazyColumn(
contentPadding = PadddingValues(...),
state = state // LazyListStatte
...
)
state.firstVisibleItemIndex // Int
state.layoutInfo // LazyListLayoutInfo
state.scrollToItem(...)
state.animateScrollToItem(...)
咱们之前了解了怎么完成具有固定列竖的笔直网格,由于列数固定,网格可用宽度除以列数后所得的值,便是每个列占用的空间量,假如还增加了spacedBy
摆放办法,就会先从可用宽度中减去距离,然后再
LazyVerticalGrid(
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangements.spaceBy(24.dp)
)
不过,运用固定列数也并非完美适合一切状况计划,在SunFlower的比如中,咱们运用了固定有两列的网格,咱们在手机上测试了这个示例运用,作用不错。
LazyVerticalGrid(
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spaceBy(24.dp),
verticalArrangement = Arrangement.spaceBy(24.dp)
) {
items(plants) {plant ->
PlantCard(plant)
}
}
但在平板电脑上运行时,作用并不是很好,图片过宽而且由于职务卡片的高度是固定的,原始图片的很大一部分被裁剪掉了。
3.1、GridCells.Adaptive
为了解决这个问题,咱们经过GridCells.Adaptive API
为列运用了自适应尺度调整功用。这样咱们就能够指定项的宽度,然后网格会容纳尽或许多的列。核算列竖后体系会将剩下的宽度均匀分配给各列。
LazyVerticalGrid(
columns = GridCells.Adaptive(128.dp),
horizontalArrangement = Arrangement.spaceBy(24.dp),
verticalArrangement = Arrangement.spaceBy(24.dp)
) {
items(plants) { plant ->
PlantCard(plant)
}
}
这种自适应尺度调整办法特别适合在不同尺度的屏幕上很好地显现多组项。
可是假如咱们有更杂乱的尺度调整要求,该怎么办?好消息是,Compose支持彻底自定义,你能够完成GridCells
。它是经过固定网格单元和自适应网格单元完成的接口。
LazyVerticalGrid(
columns = object: GridCells {
...
}
)
这个接口只有一个办法用于核算列配置。关于笔直网格,他会为你核算可用宽度以及请求的项之间的水平距离。这个办法的回来值是一个列表其间包括核算出得列宽度
LazyVerticalGrid(
columns = object: GridCells {
override fun Density.calculateCrossAxisCellSizes(
availableSize: Int,
spacing: Int,
): List<Int> {
...
}
}
)
在咱们的比如中,假定榜首列的宽度应为第二列的两倍,咱们这样核算列宽度,将榜首列的宽度调整为可用空间减去距离,再乘以2/3所得的值,第二列占有剩下空间。最终,回来包括宽度核算值的列表。能够看到列数与所回来列表的长度相符
LazyVerticalGrid(
columns = object: GridCells {
override fun Density.calculateCrossAxisCellSizes(
availableSize: Int,
spacing: Int
): List<Int> {
val firstColumn = (availableSize - spacing) * 2 / 3
val secondColumn = availableSize - spacing - firstColumn
return list(firstColumn, secondColumn(
}
}
)
3.2、实际场景
现在咱们现已了解怎么定义GridCells的结构,假如你的规划需求只让某些额项选用非标准尺度,该怎么办?来看看这个Sunflower示例运用的规划,它对植物进行了分类。咱们期望每个类别的其实方位有一个标题,显现相应类别的称号。每个标题占有行的全部宽度。
为完成这种规划,网格支持为项供给自定义列span,这能够经过网格规模DSL中item
和items
办法的span
参数指定。在这里,咱们为标题供给完整的行span。、
LazyVerticalGrid(
...
) {
item(span = {
GridItemSpan(maxLineSpan)
}) {
CategoryCard("Fruits")
}
}
maxLineSpan
,它是span规模的值之一。在运用自适应尺度调整功用时,列数不固定,maxLineSpan
就会特别有用
LazyVerticalGrid(
...
) {
item(span = {
// LazyGridItemSpanScope:
// maxLineSpan
GridItemSpan(maxLineSpan)
}) {
CategoryCard("Fruits")
}
}
在span lambda
规模中,还有一个值maxCurrentLineSpan
,它表明项在当前行中能够占有的span,当项不内行的最初时,该值与maxLineSpan
不同。
LazyVerticalGrid(
...
) {
item(span = {
// LazyGridItemSpanScope:
// maxLineSpan
// maxCurrentLineSpan = 2
GridItemSpan(maxLineSpan)
}) {
CategoryCard("Fruits")
}
}
现在来看一下植物卡片,咱们不需求明确为项供给span,因为每个项占有的span都为1,即默许值为1
LazyVerticalGrid(
...
) {
item(span = {
GridItemSpan(maxLineSpan)
}) {
CategoryCard("Fruits")
}
items(fruitPlants) { plant ->
PlantCard(plant)
}
}
不过,假定咱们需求杰出显现某些项,为此,能够设定自定义span。在这个比如中,每隔一个元素的span将为2.它是平板电脑上的呈现作用。
LazyVerticalGrid(...) {
item(span = { GridItemSpan(maxLineSpan) } { ... }
items(
fruitPlants.size,
span = { GridItemSpan(if(it.isOdd) 2 else 1) } { plant ->
PlantCard(fruitPlants[it])
}
)
}
这是它在手机上的呈现作用:
请注意,当前行中放不下的元素会自动换行显现,留下空白