在Android View中列表通常使用ListView或者RecyclerView,我们需要在xml里定义每个条目Item的布局,再创建一个适配器adapter,RecyclerView还需要设置它的LayoutManager等。
在Compose中,竖向、横向和网格列表都有自己的专门控件,使用起来更加方便简单,其中竖向列表就是用LazyColumn控件去实现,我们先看看它的简单使用:
val dataList = arrayListOf<Int>() // 数据源
for (index in 0 .. 10) {
dataList.add(index)
}
LazyColumn {
items(dataList) { data ->
Text("item:$data") // Item布局
}
}
我们创建了一个数据源list,通过其构造方法设置数据源,并指定Item布局样式为简单文本Text。
LazyListScope
我们看看LazyColumn的源码:
@Composable
fun LazyColumn(
modifier: Modifier = Modifier, // 修饰符
state: LazyListState = rememberLazyListState(), // 控制或观察列表状态的对象
contentPadding: PaddingValues = PaddingValues(0.dp), // 内容内边距
reverseLayout: Boolean = false, // 是否反向布局
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, // 垂直配列方式
horizontalAlignment: Alignment.Horizontal = Alignment.Start, // 水平排列方式
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), // 描述fling的逻辑
content: LazyListScope.() -> Unit // 描述内容的代码块
) {
// 省略...
}
有很多参数之前了解过,我们重点看看LazyListScope,它的类型是LazyListScope块,LazyColumn与Compose中大多数布局不同,它不接收@Composable内容块参数,从而允许应用程序直接发出可组合项,而是提供一个LazyListScope块。LazyListScope块提供了DSL,允许应用程序描述列表项内容。然后,LazyColumn负责按照布局和滚动位置的要求添加每个列表项的内容。我们先看看LazyListScope里面到底写了什么代码能让LazyColumn这么好用:
@LazyScopeMarker
interface LazyListScope {
/**
* 添加一个项目
*/
fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
/**
* 添加count个项目
*/
fun items(
count: Int,
key: ((index: Int) -> Any)? = null,
itemContent: @Composable LazyItemScope.(index: Int) -> Unit
)
/**
* 添加一个粘性标题,标题头将保持固定状态,直到下一个标题头取代它
*/
@ExperimentalFoundationApi
fun stickyHeader(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
}
通过LazyListScope的源码可知它是一个接口,里面有两个添加项目的方法,还有一个添加粘性标题的方法,可以看到item方法和items方法中的最后一个参数是我们熟悉的@Composable内容块参数,所以我们可以在LazyColumn中写item布局。
问题来了,item方法和items方法都不可以直接传入List或者Array,但是我们平时在项目中构建列表时使用的都是List或Array,怎么办?上面的例子我们也是通过items方法直接传入List,而上面使用的items方法并不在LazyListScope接口中,那它在哪里定义的呢?没错,就是扩展方法,LazyListScope有好几个扩展方法,我们看看:
// 添加项目列表
inline fun <T> LazyListScope.items(
items: List<T>,
noinline key: ((item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(items.size, if (key != null) { index: Int -> key(items[index]) } else null) {
itemContent(items[it])
}
// 添加项目列表,通过项目的内容可以知道其item对应的索引
inline fun <T> LazyListScope.itemsIndexed(
items: List<T>,
noinline key: ((index: Int, item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
) = items(items.size, if (key != null) { index: Int -> key(index, items[index]) } else null) {
itemContent(it, items[it])
}
// 添加项目数组
inline fun <T> LazyListScope.items(
items: Array<T>,
noinline key: ((item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(items.size, if (key != null) { index: Int -> key(items[index]) } else null) {
itemContent(items[it])
}
// 添加一个数组,通过项目的内容可以知道其item的索引
inline fun <T> LazyListScope.itemsIndexed(
items: Array<T>,
noinline key: ((index: Int, item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
) = items(items.size, if (key != null) { index: Int -> key(index, items[index]) } else null) {
itemContent(it, items[it])
}
提供的四个扩展方法,分别可以使用List和Array添加条目,如果需要获取item对应的索引,可以使用itemsIndexed扩展方法。我们一起看个例子:
val dataList = arrayListOf<String>()
for (index in 0 .. 10) {
dataList.add("ind".repeat(index))
}
LazyColumn {
itemsIndexed(dataList) { index, data ->
Text("item:第${index}个数据为$data")
}
}
根据对应的index值进行“ind”字符串的重复显示,效果如下:
使用Array的itemsIndexed
当我们用Array当做数据源时,我们就用参数为Array的LazyListScope的扩展方法,如下:
val dataList = arrayListOf<String>()
for (index in 0 .. 10) {
dataList.add("ind".repeat(index))
}
LazyColumn {
itemsIndexed(dataList.toArray()) { index, data ->
Text("item:第${index}个数据为$data")
}
}
效果同上图。
多Type
我们知道recyclerView展示多type类型的item时,我们需要创建多个样式的item布局,在适配器adapter里重写对应的方法,根据type类型创建不同的item模板来展示,比较麻烦,但是LazyColumn实现多type就比较简单,我们看看具体分为哪几步。 首先我们创建一个数据类:
data class Chat(
val content: String, // 内容
val isLeft: Boolean = true) // 是否靠左显示
再创建数据源:
val chatList = arrayListOf<Chat>()
chatList.apply {
add(Chat("你好"))
add(Chat("在家干啥呢?"))
add(Chat("出来玩啊?"))
add(Chat("没啥事", false))
add(Chat("在家呆着呢", false))
}
再用LazyColumn实现多Type展示:
LazyColumn {
items(chatList) { data ->
if (data.isLeft) {
Column(modifier = Modifier.padding(end = 15.dp)) {
Spacer(modifier = Modifier.height(5.dp))
Text(
data.content, modifier = Modifier.fillMaxWidth().height(25.dp)
.background(Color.Yellow)
)
}
} else {
Column(modifier = Modifier.padding(start = 15.dp)) {
Spacer(modifier = Modifier.height(5.dp))
Text(
data.content, modifier = Modifier.fillMaxWidth()
.background(Color.Green).height(25.dp)
)
}
}
}
}
对,就这么简单。但在真正项目中开发对应的业务时,最好将item的布局抽出来,进行状态提升,更好的提高性能。
粘性标题
粘性标题很常见,如微信中的通讯录,即使滚动通讯录列表,上方的字母索引标题固定不动,直到下一个标题取代它:
那么在Compose中怎么实现这种粘性标题效果呢?前面说的LazyListScope接口中有一个stickyHeader方法,就是做这个的。一个列表中可能只有一个标题或者一块布局需要跟随列表向上滚动,当滑动到顶部的时候这块布局需要吸附在页面顶部位置不动,而下面的列表可以继续向上滚动。这个效果在Android View中实现比较麻烦,在Compose中比较简单,来看看代码实现:
LazyColumn {
items(chatList) { item ->
Text(
item.content,
modifier = Modifier.padding(10.dp).background(Color.Red).height(150.dp)
.fillMaxWidth(), textAlign = TextAlign.Center,
fontSize = 35.sp
)
}
stickyHeader {
Text(
"粘性标题啊",
modifier = Modifier.padding(10.dp).background(Color.Green).height(150.dp)
.fillMaxWidth(), textAlign = TextAlign.Center,
fontSize = 35.sp
)
}
items(chatList) { item ->
Text(
item.content,
modifier = Modifier.padding(10.dp).background(Color.Red).height(150.dp)
.fillMaxWidth(), textAlign = TextAlign.Center,
fontSize = 35.sp
)
}
}
我们可以看到,LazyColumn中在一个粘性头部上下分别插入了列表数据,能更直观的看到粘性标题的效果。
复杂的粘性标题
如果我们要固定多个粘性标题怎么做呢?像上面说的微信通讯录一样,我们看看要哪几步去实现这个通讯录效果,首先也是创建一个数据类:
data class Contact(
val letters: String, // 字母索引
val nameList: List<String>) // 下方名字列表
数据源
val letters = arrayListOf("A", "B", "C", "D", "E")
val contactList = arrayListOf<Contact>()
val nameList = arrayListOf<String>()
for (index in 0..5) {
nameList.add("路人$index")
}
for (index in letters.iterator()) {
contactList.add(Contact(letters = index, nameList))
}
创建列表视图
LazyColumn {
contactList.forEach { (letter, nameList) ->
stickyHeader {
Text(
letter,
modifier = Modifier.padding(10.dp).background(Color.Green)
.fillMaxWidth(), textAlign = TextAlign.Center,
fontSize = 35.sp
)
}
items(nameList) { contact ->
Text(
contact,
modifier = Modifier.padding(10.dp).background(Color.Red).height(50.dp)
.fillMaxWidth(), textAlign = TextAlign.Center,
fontSize = 35.sp
)
}
}
}
效果如下:
列表回到顶部
现在很多场景,点击手机状态栏会让列表滚动到最初始的位置,Android View中的RecyclerView调用smoothScrollToPosition(0)即可。在Compose中,因为是数据驱动UI显示,通过监听State的改变从而改变页面,这里也是。我们知道LazyColumn的方法体中有一个参数LazyListState,这就是LazyColumn的State,我们通过改变它的值value从而触发页面的刷新。 首先:我们创建LazyListState
val listState = rememberLazyListState()
只需要调用rememberLazyListState方法就可以创建一个LazyListState,然后可以将它传入到LazyColumn中:
LazyColumn(state = listState) {
// 省略...
}
调用LazyListState的scrollToItem或animateScrollToItem方法就可以滚动到指定的位置:
val coroutineScope = rememberCoroutineScope()
Button(
modifier = Modifier.width(200.dp).height(40.dp),
onClick = {
coroutineScope.launch {
listState.animateScrollToItem(index = 0)
}
}) {
Text("点击回到顶部")
}
这里需要注意,animateScrollToItem是挂起函数(suspend修饰的):
suspend fun animateScrollToItem(
/*@IntRange(from = 0)*/
index: Int,
/*@IntRange(from = 0)*/
scrollOffset: Int = 0
) {
doSmoothScrollToItem(index, scrollOffset)
}
我们需要在协程中使用,所以我们通过rememberCoroutineScope方法创建一个协程作用域,在里面调用回调顶部的方法,实现效果:
完整代码:
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
val letters = arrayListOf("A", "B", "C", "D", "E")
val contactList = arrayListOf<Contact>()
val nameList = arrayListOf<String>()
for (index in 0..5) {
nameList.add("路人$index")
}
for (index in letters.iterator()) {
contactList.add(Contact(letters = index, nameList))
}
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(state = listState) {
contactList.forEach { (letter, nameList) ->
stickyHeader {
Text(
letter,
modifier = Modifier
.padding(10.dp)
.background(Color.Green)
.fillMaxWidth(), textAlign = TextAlign.Center,
fontSize = 35.sp
)
}
items(nameList) { contact ->
Text(
contact,
modifier = Modifier
.padding(10.dp)
.background(Color.Red)
.height(50.dp)
.fillMaxWidth(), textAlign = TextAlign.Center,
fontSize = 35.sp
)
}
}
}
Button(
modifier = Modifier.width(200.dp).height(40.dp),
onClick = {
coroutineScope.launch {
listState.animateScrollToItem(index = 0)
}
}) {
Text("点击回到顶部")
}
}
喜欢的点个赞收藏下吧。代码已上传的github:github.com/Licarey/com…