Cloumn 和 Row
如果是普通的不是特别长的列表,可以直接使用 Column
和 Row
组件,默认 Column
和 Row
组件是不支持滚动的,如果需要支持滚动可以在 Column
和 Row
组件上使用 Modifier.verticalScroll()
和Modifier.horizontalScroll()
修饰符
@Composable
fun ColumnList(list: List<String>) {
Box {
Column(Modifier.verticalScroll(rememberScrollState())) {
list.forEach {
Text(it)
Divider()
}
}
}
}
@Composable
fun RowList(list: List<String>) {
Box {
Row(Modifier.horizontalScroll(rememberScrollState())) {
list.forEach {
Text(it)
Divider(Modifier.width(1.dp).fillMaxHeight(), thickness = 1.dp)
}
}
}
}
Column 的 verticalArrangement
参数可以指定item在主轴不同的排列方式:
@Composable
fun ColumnExample() {
Column(
modifier = Modifier.border(1.dp, Color.Blue),
// verticalArrangement = Arrangement.Top // 默认是Top
) {
Text("默认效果", Modifier.background(Color.Green))
Text("默认效果", Modifier.background(Color.Green))
Text("默认效果", Modifier.background(Color.Green))
}
}
@Composable
fun ColumnExample2() {
Column(
modifier = Modifier.border(1.dp, Color.Blue),
verticalArrangement = Arrangement.Center
) {
Text("Arrangement.Center", Modifier.background(Color.Green))
Text("Arrangement.Center", Modifier.background(Color.Green))
Text("Arrangement.Center", Modifier.background(Color.Green))
}
}
@Composable
fun ColumnExample201() {
Column(
modifier = Modifier.border(1.dp, Color.Blue),
verticalArrangement = Arrangement.SpaceAround
) {
Text("Arrangement.SpaceAround", Modifier.background(Color.Green))
Text("Arrangement.SpaceAround", Modifier.background(Color.Green))
Text("Arrangement.SpaceAround", Modifier.background(Color.Green))
}
}
@Composable
fun ColumnExample202() {
Column(
modifier = Modifier.border(1.dp, Color.Blue),
verticalArrangement = Arrangement.SpaceBetween
) {
Text("Arrangement.SpaceBetween", Modifier.background(Color.Green))
Text("Arrangement.SpaceBetween", Modifier.background(Color.Green))
Text("Arrangement.SpaceBetween", Modifier.background(Color.Green))
}
}
@Composable
fun ColumnExample203() {
Column(
modifier = Modifier.border(1.dp, Color.Blue),
verticalArrangement = Arrangement.SpaceEvenly
) {
Text("Arrangement.SpaceEvenly", Modifier.background(Color.Green))
Text("Arrangement.SpaceEvenly", Modifier.background(Color.Green))
Text("Arrangement.SpaceEvenly", Modifier.background(Color.Green))
}
}
@Composable
fun ColumnExample204() {
Column(
modifier = Modifier.border(1.dp, Color.Blue),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text("Arrangement.spacedBy(10.dp)", Modifier.background(Color.Green))
Text("Arrangement.spacedBy(10.dp)", Modifier.background(Color.Green))
Text("Arrangement.spacedBy(10.dp)", Modifier.background(Color.Green))
}
}
@Composable
fun ColumnExample205() {
Column(
modifier = Modifier.border(1.dp, Color.Blue),
verticalArrangement = Arrangement.Bottom
) {
Text("Arrangement.Bottom", Modifier.background(Color.Green))
Text("Arrangement.Bottom", Modifier.background(Color.Green))
Text("Arrangement.Bottom", Modifier.background(Color.Green))
}
}
下面的动图总结了不同垂直排列方式的效果:
同样,通过 horizontalAlignment
参数可以指定item在交叉轴(横轴)上的排列方式:
@Composable
fun ColumnExample3() {
Column(
modifier = Modifier.border(1.dp, Color.Blue),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("ffff", Modifier.background(Color.Green))
Text("Alignment.CenterHorizontally", Modifier.background(Color.Green))
}
}
@Composable
fun ColumnExample4() {
Column(
modifier = Modifier.border(1.dp, Color.Blue),
horizontalAlignment = Alignment.End
) {
Text("kkk", Modifier.background(Color.Green))
Text("Alignment.End", Modifier.background(Color.Green))
}
}
Row 组件也可以通过 horizontalArrangement
参数指定item在水平方向的排列方式:
@Composable
fun RowExample() {
val modifier = Modifier
.padding(5.dp)
.clip(RoundedCornerShape(35))
.background(Color.Green)
val modifier2 = modifier
.width(50.dp)
.padding(5.dp)
Column {
Divider()
Text("Equal Weight")
Row {
Box(modifier
.weight(1f)
.padding(5.dp)) {
Text(text = "A", Modifier.align(Alignment.Center))
}
Box(modifier
.weight(1f)
.padding(5.dp)) {
Text(text = "B", Modifier.align(Alignment.Center))
}
Box(modifier
.weight(1f)
.padding(5.dp)) {
Text(text = "C", Modifier.align(Alignment.Center))
}
}
Divider()
Text("Arrangement.Start")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
Box(modifier2) {
Text(text = "A", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "B", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "C", Modifier.align(Alignment.Center))
}
}
Divider()
Text("Arrangement.Center")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Box(modifier2) {
Text(text = "A", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "B", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "C", Modifier.align(Alignment.Center))
}
}
Divider()
Text("Arrangement.End")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Box(modifier2) {
Text(text = "A", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "B", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "C", Modifier.align(Alignment.Center))
}
}
Divider()
Text("Arrangement.SpaceBetween")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Box(modifier2) {
Text(text = "A", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "B", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "C", Modifier.align(Alignment.Center))
}
}
Divider()
Text("Arrangement.SpaceAround")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
Box(modifier2) {
Text(text = "A", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "B", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "C", Modifier.align(Alignment.Center))
}
}
Divider()
Text("Arrangement.SpaceEvenly")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Box(modifier2) {
Text(text = "A", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "B", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "C", Modifier.align(Alignment.Center))
}
}
Divider()
Text("Arrangement.spacedBy(20.dp)")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(20.dp)
) {
Box(modifier2) {
Text(text = "A", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "B", Modifier.align(Alignment.Center))
}
Box(modifier2) {
Text(text = "C", Modifier.align(Alignment.Center))
}
}
Divider()
}
}
下面的动图总结了不同水平排列方式的效果:
LazyColumn 和 LazyRow
这两个是支持惰性加载的列表组件,只有屏幕显示的部分才会真正被加载,对标传统View中的RecyclerView
@Composable
fun LazyColumnList() {
LazyColumn(
Modifier
.fillMaxSize()
.background(Color.Gray),
contentPadding = PaddingValues(35.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(50) { index ->
CardContent("我是序号第 $index 的卡片")
}
}
}
在LazyList组件中是通过使用items
或item
这两个DSL来添加item组件的,通过item
DSL可以很方便的指定 header 和 footer
@Composable
fun LazyColumnList2() {
val list = listOf("A", "B", "C")
LazyColumn(
Modifier.fillMaxSize(),
contentPadding = PaddingValues(35.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
item {
Text("Header 第一项")
}
items(5) { index ->
Text("第 ${index + 2} 项")
}
itemsIndexed(list) { index, s ->
Text("第 ${index + 7} 项 $s")
}
item {
Text("Footer 最后一项")
}
}
}
粘性 header
粘性header通过 stickyHeader
这个DSL来实现:
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyColumnList5() {
val sections = listOf("A", "B", "C", "D", "E", "F", "G")
LazyColumn(
/*reverseLayout = true, */
contentPadding = PaddingValues(15.dp)
) {
sections.forEach { section ->
stickyHeader {
Text(
"Section $section 粘性Header",
Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(8.dp)
)
}
items(10) {
CardContent("Item $it from the section $section")
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyRowExample() {
LazyRow(
Modifier
.fillMaxWidth()
.background(Color.Gray),
contentPadding = PaddingValues(15.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
stickyHeader {
Box(Modifier
.height(150.dp)
.background(Color.Red)
.padding(8.dp)) {
Text(text = "粘性Header", color = Color.White, fontSize = 20.sp)
}
}
items(50) { index ->
CardContent2(index)
}
}
}
如需实现具有多个标题的列表(例如“联系人列表”),可以执行以下操作,先通过groupBy
进行分组:
// TODO: This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
LazyColumn {
grouped.forEach { (initial, contactsForInitial) ->
stickyHeader {
CharacterHeader(initial)
}
items(contactsForInitial) { contact ->
ContactListItem(contact)
}
}
}
}
自定义排列方式
通常,延迟列表包含许多项,并且这些项所占空间大于滚动容器的大小。不过,如果列表中填充的项很少,那么在设计中,您可以对这些项在视口中的位置做出更具体的要求。
@Composable
fun TopWithFooterExample() {
val list = listOf("A", "B", "C", "I am the Footer")
LazyColumn(
Modifier.fillMaxHeight(),
contentPadding = PaddingValues(15.dp),
verticalArrangement = TopWithFooter // 使用自定义垂直 Arrangement
) {
itemsIndexed(list) { index, item ->
Box(modifier = Modifier
.background(Color.Blue)
.padding(10.dp)) {
Text("第 $index 项 $item", fontSize = 20.sp, color = Color.White)
}
}
}
}
object TopWithFooter : Arrangement.Vertical {
// 重写 arrange 方法
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()
}
}
}
contentPadding 内容边距
使用LazyList
时,如果使用普通的 Modifier.padding()
为其添加内边距,效果不会像我们期望的那样,而是会在滑动到边界位置时内容出现padding大小的截断:
@Composable
fun AlignYourBodyRow(
modifier: Modifier = Modifier
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
// 使用修饰符修改左右的padding会导致滑动的时候出现截断
modifier = modifier.padding(horizontal = 16.dp),
) {
items(alignYourBodyData) { item ->
AlignYourBodyElement(item.drawable, item.text)
}
}
}
此时正确的做法应当使用 LazyList
的 contentPadding
属性来设置内边距:
@Composable
fun AlignYourBodyRow(
modifier: Modifier = Modifier
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 16.dp), // 为列表设置正确的内边距
modifier = modifier
) {
items(alignYourBodyData) { item ->
AlignYourBodyElement(item.drawable, item.text)
}
}
}
可以仔细观察对比一下二者的不同。
显示返回顶部按钮
通过 scrollState.firstVisibleItemIndex > 0
判断是否向下滚动了,然后通过 scrollState.scrollToItem(0)
返回顶部
@Composable
fun LazyColumnList3() {
val scrollState = rememberLazyListState(15) // 从index=15的开始显示
Box {
LazyColumn(
Modifier
.fillMaxHeight()
.background(Color.Gray),
verticalArrangement = Arrangement.spacedBy(10.dp),
state = scrollState
) {
items(50) { index ->
CardContent("我是序号第 $index 的卡片")
}
}
val showScrollToTopButton by remember {
derivedStateOf { scrollState.firstVisibleItemIndex > 10 } // 这里根据实际业务一页大概展示多少条,可以判断超过一页后再显示
}
if (showScrollToTopButton) {
val scope = rememberCoroutineScope()
ExtendedFloatingActionButton(
text = {
Column {
Icon(Icons.Default.KeyboardArrowUp, null)
Text("Top")
}
},
onClick = {
scope.launch { scrollState.scrollToItem(0) }
},
shape = CircleShape, // RoundedCornerShape(15),
modifier = Modifier.size(70.dp).align(Alignment.BottomEnd),
backgroundColor = Color.Green
)
}
}
}
可以使用 AnimatedVisibility
为按钮添加动画:
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun LazyColumnList3() {
val scrollState = rememberLazyListState(15) // 从index=15的开始显示
Box {
LazyColumn(
Modifier
.fillMaxHeight()
.background(Color.Gray),
verticalArrangement = Arrangement.spacedBy(10.dp),
state = scrollState
) {
items(50) { index ->
CardContent("我是序号第 $index 的卡片")
}
}
val showScrollToTopButton by remember {
derivedStateOf { scrollState.firstVisibleItemIndex > 10 }
}
AnimatedVisibility(
visible = showScrollToTopButton,
modifier = Modifier.align(Alignment.BottomEnd),
enter = fadeIn() + scaleIn(),
exit = fadeOut() + scaleOut(),
) {
val scope = rememberCoroutineScope()
ExtendedFloatingActionButton(
text = {
Column {
Icon(Icons.Default.KeyboardArrowUp, null)
Text("Top")
}
},
onClick = {
scope.launch { scrollState.scrollToItem(0) }
},
shape = CircleShape, // RoundedCornerShape(15),
modifier = Modifier.size(70.dp),
backgroundColor = Color.Green
)
}
}
}
使用 scrollToItem
或 animateScrollToItem
时发现,有时不一定能够滑动到顶部,可以通过指定滑动的距离解决,如 animateScrollToItem(0, -10000)
或者 scrollBy(-10000f)
LazyListState
可以帮助我们分析滑动事件, 一个常见的例子是,系统会在用户滚动经过某个点后发送分析事件。 为了高效地解决此问题,我们可以使用 snapshotFlow()
:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
LazyListState
还可以通过 layoutInfo
属性提供有关当前显示的所有列表项以及这些项在屏幕上的边界的信息。
contentType 多类型列表
从 Compose 1.2
开始,为了最大限度地提高延迟布局的性能,建议将 contentType
添加到列表或网格中。 当列表或网格由多种不同类型的项组成时,这样可为布局的每一项指定内容类型:
@Composable
fun MultiTypeList() {
val list by remember { mutableStateOf(prepareNewsList()) }
LazyColumn(
contentPadding = PaddingValues(15.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(list, contentType = { it.type }) { item ->
if (item.type == 1) {
Card(elevation = 8.dp, modifier = Modifier.fillMaxWidth()) {
Row(
horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.ic_sky),
contentDescription = null,
modifier = Modifier.size(100.dp),
contentScale = ContentScale.Crop
)
Text(item.name.repeat(2), fontSize = 20.sp)
}
}
} else {
CardContent(text = item.name.repeat(3))
}
}
}
}
fun prepareNewsList() : List<NewsItem> {
return List(50) {
NewsItem("NewsItem $it ", if(it % 2 == 0) 1 else 2)
}
}
data class NewsItem(val name : String, val type : Int)
延迟列表的使用注意事项
为列表项指定 key
注意:实际业务中使用LazyColumn
必须为每个item
提供一个稳定的 唯一key
(默认是将position
作为索引)
键的类型必须受 Bundle
支持,这是 Android 的机制,旨在当重新创建 activity
时保持相应状态。
Bundle
支持基本类型、枚举或 Parcelable
等类型。
Bundle
必须支持该键,以便在重新创建 activity 时,甚至在滚动离开此项然后滚动回来时,此项可组合项中的 rememberSaveable
仍可以恢复。
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(
items = messages,
key = { message ->
// Return a stable + unique key for the item
message.id
}
) { message ->
MessageRow(message)
}
}
}
避免使用大小为 0 像素的列表项
如果使用分页,尤其是在加载图片列表时,必须为每个列表项预先提供一个固定高度大小的占位符,因为LazyColumn
会在首次测量时认为高度没有限制,会一直进行组合,直到计算的测量高度填满可用的视图窗口,然后停止组合。
如果没有提供占位符(高度是0px
)或者提供了高度很小的占位符,则意味着在首次测量时,LazyColumn
会组合它的所有项,因为高度为0
的项可以很容易容纳到当前窗口中。而在几毫秒之后,图片又加载出来了,LazyColumn
开始重组显示图片,但此时只有一部分能被容纳到窗口中,因此LazyColumn
会舍弃最开始组合的不必要的其他所有项。为了避免这种性能损耗,应该为每个item
提供一个默认的高度大小的占位符,最好的做法是保证加载前后的item高度大小不变。
避免嵌套可以同方向滚动的组件
这仅适用于将没有预定义尺寸的可滚动子级嵌套在可向同一方向滚动的另一个父级中的情况。
例如,尝试在可垂直滚动的 Column
父级中嵌套没有固定高度的子级 LazyColumn
:
// Throws IllegalStateException
Column(
modifier = Modifier.verticalScroll(state)
) {
LazyColumn {
// ...
}
}
Compose不支持嵌套同方向的滚动列表,比如LazyColumn
里嵌套LazyColumn
,这种小心思就别想了,Google不让你这么玩~
但是可以在Row
中嵌套LazyColumn
,如果一定要在Column
中嵌套LazyColumn
,必须为LazyColumn
指定固定的大小:
Column(
modifier = Modifier.verticalScroll(scrollState)
) {
LazyColumn(
modifier = Modifier.height(200.dp)
) {
// ...
}
}
尽量避免将多个元素放入一个 item{...}
项中
LazyVerticalGrid(
// ...
) {
item { Item(0) }
item {
Item(1)
Item(2)
}
item { Item(3) }
// ...
}
在此示例中,第二个项 lambda 在一个代码块中发出 2 个项,当多个元素作为一个项的一部分发出时,系统会将其作为一个实体进行处理,这意味着这些元素无法再单独组合。
也就是说当其中一个元素需要重组时,系统会将lambda项中的所有元素都进行重组。如果过度使用,则可能会降低性能。
将所有元素放入一个项属于极端情况,完全违背了使用延迟布局的目的。除了潜在的性能问题外,这种情况还会干扰 scrollToItem()
和 animateScrollToItem()
。如上面代码如果调用scrollToItem(index = 2)
则实际会滑动到Item(3)
的位置,因为Item(1)
和Item(2)
被视为一个项。
不过,也有一些将多个元素放入一个项的有效用例,例如在一个列表内添加多条分隔线。你可能不希望分隔线更改滚动索引,因为分割线不应被视为独立元素。此外,由于分隔线占空间很小,因此性能不会受到影响。
分隔线可能需要在其之前的那个项可见时显示,这样分割线就可纳入前一个项中:
LazyVerticalGrid(
// ...
) {
item { Item(0) }
item {
Item(1)
Divider()
}
item { Item(2) }
// ...
}
此外,如果要测试延时列表的性能应当在release
版本的模式下测试,不应该测试debug
版本(并且是在真机而不是模拟器上测试,多次测试结果)
LazyVerticalGrid 和 LazyHorizontalGrid
可以滑动的表格组件,同时支持Column和Row的verticalArrangement
和 horizontalArrangement
属性效果,可以通过 columns
和 rows
属性指定列数或行数,例如 GridCells.Fixed(3)
@Composable
fun LazyVerticalGridExample() {
val itemsIndexedList = ('A'..'Z').toList()
LazyVerticalGrid(
columns = GridCells.Fixed(3),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(5.dp)
) {
itemsIndexed(itemsIndexedList) { index, item ->
GridItem("Item at $index is $item")
}
}
}
@Composable
fun LazyHorizontalGridExample() {
val itemsIndexedList = ('A'..'Z').toList()
LazyHorizontalGrid(
rows = GridCells.Fixed(3),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(5.dp)
) {
itemsIndexed(itemsIndexedList) { index, item ->
GridItem("Item at $index is $item")
}
}
}
设置单元格的大小占比权重
可以通过 span
参数设置每个单元格的大小占比权重,span
默认值是 1, 将 span
设置为不同的值可以实现简单的瀑布流效果:
@Composable
fun LazyVerticalGridExample2_1() {
val list = ('A'..'Z').toList()
val spanList = listOf(1, 2, 1, 3, 1, 1, 3, 4, 2, 2, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 3, 1, 1, 1)
LazyVerticalGrid(
columns = GridCells.Fixed(4),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(5.dp)
) {
itemsIndexed(list, span={index, item -> GridItemSpan( spanList[index])}) { index, item ->
Text(
"Item $item",
modifier = Modifier
.clip(RoundedCornerShape(5.dp))
.background(colors[index % colors.size])
.height(80.dp)
.wrapContentSize()
.padding(5.dp),
color = Color.White,
fontSize = 16.sp
)
}
}
}
如果设置 span = GridItemSpan(maxLineSpan)
则会独占一整行,可以用来实现常见的分组标题的效果:
@Composable
fun LazyVerticalGridExample2() {
val sections = (0 until 25).toList().chunked(5)
LazyVerticalGrid(
columns = GridCells.Fixed(4),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(5.dp)
) {
sections.forEachIndexed { index, items ->
// maxLineSpan 是 LazyGridItemSpanScope 的值之一
// 设置 span = GridItemSpan(maxLineSpan) 即独占一整行
item(span = { GridItemSpan(maxLineSpan) }) {
Text(
"This is section $index",
Modifier
.clip(RoundedCornerShape(5.dp))
.background(MaterialTheme.colorScheme.primary)
.height(50.dp)
.wrapContentSize(),
color = Color.White,
fontSize = 16.sp
)
}
items(
items,
span = { GridItemSpan( if(it % 2 == 0) 1 else 2) }
) {
GridItem("Item $it")
}
}
}
}
@Composable
fun LazyHorizontalGridExample2() {
val sections = (0 until 25).toList().chunked(5)
LazyHorizontalGrid(
rows = GridCells.Fixed(3),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(5.dp)
) {
sections.forEachIndexed { index, items ->
item(span = { GridItemSpan(maxLineSpan) }) {
Text(
"section $index",
Modifier
.clip(RoundedCornerShape(5.dp))
.background(MaterialTheme.colorScheme.primary)
.wrapContentSize()
.padding(8.dp),
color = Color.White,
fontSize = 16.sp
)
}
items(
items,
// not required as it is the default
span = { GridItemSpan(1) }
) {
GridItem("Item $it")
}
}
}
}
GridCells.Adaptive() 自适应屏幕
例如,以下代码设置每个单元格至少100dp
, 这在旋转屏幕场景下会自适应:
@Composable
fun LazyVerticalGridExample3() {
val itemsIndexedList = ('A'..'Z').toList()
LazyVerticalGrid(
columns = GridCells.Adaptive(100.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(5.dp)
) {
itemsIndexed(itemsIndexedList) { index, item ->
GridItem("Item at $index is $item")
}
}
}
自定义columns的每一列的宽度占比
@Composable
fun LazyVerticalGridExample4() {
val itemsIndexedList = ('A'..'Z').toList()
LazyVerticalGrid(
columns = object: GridCells {
override fun Density.calculateCrossAxisCellSizes(
availableSize: Int,
spacing: Int
): List<Int> {
// 将第一列的宽度调整为可用空间的2/3,第二列的宽度调整为可用空间的1/3
val firstColumn = (availableSize - spacing) * 2 / 3
val secondColumn = availableSize - spacing - firstColumn
return listOf(firstColumn, secondColumn)
}
},
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(5.dp)
) {
itemsIndexed(itemsIndexedList) { index, item ->
GridItem("Item at $index is $item")
}
}
}
LazyHorizontalStaggeredGrid 和 LazyVerticalStaggeredGrid 瀑布流
LazyHorizontalStaggeredGrid
和 LazyVerticalStaggeredGrid
主要为了支持瀑布流效果
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyVerticalStaggeredGridExample() {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(3),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
){
itemsIndexed(list){ index, item ->
Card(
shape = RoundedCornerShape(4.dp),
backgroundColor = colors[index % colors.size],
) {
Text(
text = "$index $item",
color = Color.White,
modifier = Modifier.padding(10.dp)
)
}
}
}
}
LazyVerticalStaggeredGrid
的单元格同样支持Adaptive
自适应属性, 例如将上面的代码中 columns
属性改成 StaggeredGridCells.Adaptive(80.dp)
,则效果如下:
LazyHorizontalStaggeredGrid
使用是类似的:
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyHorizontalStaggeredGridExample() {
LazyHorizontalStaggeredGrid(
rows = StaggeredGridCells.Adaptive(80.dp),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.Center,
horizontalArrangement = Arrangement.spacedBy(8.dp)
){
itemsIndexed(list){ index, item ->
Card(
shape = RoundedCornerShape(4.dp),
backgroundColor = colors[index % colors.size],
) {
Box {
Text(
text = "$index $item",
color = Color.White,
modifier = Modifier.padding(10.dp).align(Alignment.Center)
)
}
}
}
}
}
LazyHorizontalStaggeredGrid
和 LazyVerticalStaggeredGrid
中的 items
DSL目前不支持设置span
参数,这意味着不能在固定列数/行数的情况下指定某个item占据多个行或多个列。也就是目前还无法实现如下效果:
如果想要实现此效果,只能使用前面的 LazyVerticalGrid
然后结合 Row
或Column
甚至是ConstraintLayout
来设置每个出现横纵交错的内容块里面的布局,比较麻烦,自适应性肯定不好,还是希望官方能给出支持。
下拉刷新 Modifier.pullRefresh
从Compose 1.3.0
开始,SDK自带了对下拉刷新的支持(在此之前的版本是通过Accompanist库中提供的SwipeRefresh组件),通过在Box
组件上应用修饰符Modifier.pullRefresh
即可。
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullToRefreshModifier() {
val refreshScope = rememberCoroutineScope()
var refreshing by remember { mutableStateOf(false) }
var itemCount by remember { mutableStateOf(15) }
fun refresh() = refreshScope.launch {
refreshing = true
delay(1000)
itemCount += 5
refreshing = false
}
val state = rememberPullRefreshState(refreshing, ::refresh)
Box(Modifier.pullRefresh(state)) {
LazyColumn(Modifier.fillMaxSize()) {
items(itemCount) {
ListItem { Text(text = "Item ${itemCount - it}") }
}
}
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
}
}
其中rememberPullRefreshState
需要传递一个refreshing
标记状态以及一个refresh
方法,refresh
是请求刷新数据的业务逻辑,实际项目中,应该放在ViewModel
中:
val viewModel: MyViewModel = viewModel()
val refreshing by viewModel.isRefreshing
val pullRefreshState = rememberPullRefreshState(refreshing, { viewModel.refresh() })
Box(Modifier.pullRefresh(pullRefreshState)) {
LazyColumn(Modifier.fillMaxSize()) {
...
}
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
}
此外,Modifier.pullRefresh
还有一个重载函数,可以提供 onPull
和 onRelease
两个动作的回调,方便我们在不同的触发时机进行一些业务操作,如显示自定义的Indicator
等。
@ExperimentalMaterialApi
fun Modifier.pullRefresh(
onPull: (pullDelta: Float) -> Float,
onRelease: suspend (flingVelocity: Float) -> Unit,
enabled: Boolean = true
): Modifier
以下代码在下拉时根据下拉距离改变顶部的水平进度条的进度,在释放时进行刷新请求数据并执行动画
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullToRefreshModifier2() {
val refreshScope = rememberCoroutineScope()
val threshold = 160.dp.toPx()
var refreshing by remember { mutableStateOf(false) }
var itemCount by remember { mutableStateOf(15) }
var currentDistance by remember { mutableStateOf(0f) }
val progress = currentDistance / threshold
fun refresh() = refreshScope.launch {
refreshing = true
delay(1000)
itemCount += 5
refreshing = false
}
fun onPull(pullDelta: Float): Float = when {
refreshing -> 0f
else -> {
val newOffset = (currentDistance + pullDelta).coerceAtLeast(0f)
val dragConsumed = newOffset - currentDistance
currentDistance = newOffset
dragConsumed
}
}
suspend fun onRelease() {
if (refreshing) return // Already refreshing - don't call refresh again.
if (currentDistance > threshold) refresh()
animate(
initialValue = currentDistance,
targetValue = 0f,
animationSpec = tween(1000, easing = LinearOutSlowInEasing)
) { value, _ ->
currentDistance = value
}
}
Box(Modifier.pullRefresh(::onPull, { onRelease() })) {
LazyColumn {
items(itemCount) {
ListItem { Text(text = "Item ${itemCount - it}") }
}
}
// Custom progress indicator
AnimatedVisibility(visible = (refreshing || progress > 0)) {
if (refreshing) {
LinearProgressIndicator(Modifier.fillMaxWidth())
} else {
LinearProgressIndicator(progress, Modifier.fillMaxWidth())
}
}
}
}
将上面代码中的LinearProgressIndicator
换成CircularProgressIndicator
,还可以根据下拉的距离改变圆形Indicator
到顶部的距离
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullToRefreshModifier2() {
.......
Box(Modifier.pullRefresh(::onPull, { onRelease() })) {
LazyColumn {
items(itemCount) {
ListItem { Text(text = "Item ${itemCount - it}") }
}
}
if (currentDistance > 0) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.TopCenter)
.size(50.dp)
.offset(y = if (refreshing) 0.dp else currentDistance.toDp()),
color = Color.Red,
strokeWidth = 3.dp
)
}
}
}
官方还提供了一个很方便的修饰符 Modifier.pullRefreshIndicatorTransform
来根据PullRefreshState
自动改变Indicator
的平移位置和缩放大小
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullToRefreshModifier4() {
val refreshScope = rememberCoroutineScope()
var refreshing by remember { mutableStateOf(false) }
var itemCount by remember { mutableStateOf(15) }
fun refresh() = refreshScope.launch {
refreshing = true
delay(1500)
itemCount += 5
refreshing = false
}
val state = rememberPullRefreshState(refreshing, ::refresh)
val rotation = animateFloatAsState(state.progress * 120)
Box(Modifier.fillMaxSize().pullRefresh(state)) {
LazyColumn {
items(itemCount) {
ListItem { Text(text = "Item ${itemCount - it}") }
}
}
Surface(
modifier = Modifier
.size(50.dp)
.align(Alignment.TopCenter)
.pullRefreshIndicatorTransform(state, scale = true)
.rotate(rotation.value),
shape = RoundedCornerShape(10.dp),
color = Color.DarkGray,
elevation = if (state.progress > 0 || refreshing) 20.dp else 0.dp,
) {
Box {
if (refreshing) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(30.dp),
color = Color.White,
strokeWidth = 3.dp
)
}
}
}
}
}