Jetpack Compose | Compose 滑动列表真的需要使用LazyColumn吗?No No No!
Jetpack Compose | 控件篇(五)-- Spacer、LazyRow、LazyColumn & 让Column可滑动
在上一篇中,我们完成了 Box、Row、Column
相关内容的学习,并且留下了一个疑问:"如果容器大小不足以承载内容,怎么处理呢?",这一篇我们一起学习这部分内容。
文中代码均基于 1.0.1版本
如无特殊说明,文中的
Compose
均指代Jetpack compose
文中代码均可在 WorkShop 中获取,本篇代码集中于 post29 & post30 包下
完整系列目录: Github Pages | 掘金 | csdn
和Android进行简单对比
在Android中,SDK提供了诸如: ScrollView
NestedScrollView
ListView
GridView
RecyclerView
,等针对各类场景下适用的控件, 基于滑动手势调整内容展示区域,以达到显示更多内容的目的。
进一步探索可以发现:View本身就包含了Scroll机制的 半成品实现
,当然,本文我们不去深究Android的内容,借助我们已经掌握的Android知识,引出一点:
基于Scroll机制,用小容器展现大内容的本质:在视图测量的基础上,结合滑动手势处理,调整内容布局,绘制后展现。
在早期的一些文章中,有博主提到:Compose中对应的内容为 ScrollRow,ScrollColumn
/ LazyRow、LazyColumn
。
在早期的预览版中,短暂的存在过 ScrollRow,ScrollColumn等内容,似乎已经被移除
Compose中也是按照这样的思路设计的,我们将在后续的文章中再细致地展开研究,本篇中仅学习如何使用它们。
在真正开始这部分内容之前,先补充一个简单的控件 Spacer,可以简单的创建占位间距,后续的文章中已经没有他的位置了。
Spacer
在之前的文章中,我们学习过Modifier,其中包含一些和布局相关的API,例如:padding
、offset
,但并无 margin
等内容,按照业内惯例, 如果已经存在一个广为接受的名词,一般不会使用新词,至少词根是一致的 ,在Compose中,使用了Spacer,取缔了Margin的一些使用场景。
注:计算总是有损耗的,不要滥用Spacer,并且很多场景下有特定的方式处理间距,后续会逐渐学习到
如何使用
@Composable
fun Spacer(modifier: Modifier)
一般只需要指定他的宽高尺寸即可,例如:
Spacer(modifier = Modifier.size(3.dp))
LazyColumn
在上一篇文章中,我们已经学习过 Row和Column,它们仅仅是在方向上不一致,在实现上非常类似。同样的,LazyRow和LazyColumn也是如此。
Doc中提到:
The vertically scrolling list that only composes and lays out the currently visible items. The content block defines a DSL which allows you to emit items of different types. For example you can use LazyListScope.item to add a single item and LazyListScope.items to add a list of items.
仅 组合计算 以及 布局 当前可见元素的纵向可滑动列表。内容块定义了一个DSL,允许创建不同类型的元素。
例如:使用 LazyListScope.item
添加单个元素, LazyListScope.items
添加元素列表。
注:"内容块定义了一个DSL,允许创建不同类型的元素",这并不同于Android中概念: RecyclerView#Adapter
具有将数据映射为同类型 ViewHolder
或 不同类型 ViewHolder
的能力。而是指 "添加元素时可以是 单个元素
,或者是 元素的列表
,这是不同的类型。 字面翻译在中文语境下容易造成误解。
很显然,它类似于Android中的 ListView
,RecyclerView
, 着重点在于 Lazy
,不会将元素一股脑的全计算、布局出来。
所以,它并不对标ScrollView,在它的使用场景下,可滑动需求非常普遍,便默认实现了!
我们前面提到:
在早期的一些文章中,有博主提到:Compose中对应的内容为 ScrollRow,ScrollColumn / LazyRow、LazyColumn
这本没有啥错误,但绝不是被曲解的: "Row和Column 无法提供滑动能力,而是需要使用 LazyRow、LazyColumn"
但气氛已经烘托到这里了,那我们先将其学完,再学习 Row和Column 如何提供滑动能力。
如何使用
@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(),
content: LazyListScope.() -> Unit
)
各个参数含义:
- modifier:修饰器
- state:用于控制或者观测列表状态
- contentPadding:整体内容周围的一个Padding,注:内容四周的留白,以纵向列表为例,尾部没有展示时看不到尾部的留白 这通过Modifier无法实现, 注:Modifier只能实现列表容器固定的留白间距 。可以使用它在第一个元素前和最后一个元素后留白。 如果需要在元素间留出间距,可以使用 verticalArrangement
- reverseLayout:是否反转列表
- verticalArrangement:子控件纵向的范围。可用于添加子控件之间的间距,以及内容不足以填满列表最小尺寸时,如何排布
- horizontalAlignment:子控件横向对齐方式
- flingBehavior:fling行为的处理逻辑
- content:声明了如何提供子控件的DSL,有两种方式
@LazyScopeMarker
interface LazyListScope {
fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
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)
}
顺带一提:笔者参与的上一个项目中,高频使用RecycleView用作内容呈现,为了便捷的处理 "item之间的间距"、"首尾留白"、"特定item间不应用间距", 在项目中写了一套部件,后续可以拆出来同大家分享下。
基于LazyListScope.item 方法
在上一篇文章对应的WorkShop内容中,已经出现了这一用法 post29包下。
例如:
private fun LazyListScope.rowDemo() {
item {
CodeSample(code = "row sample 1:")
Row {
// ignore
}
}
item {
CodeSample(code = "row sample 2:纵向居中对齐")
// ignore
}
// ignore
}
基于LazyListScope.items 方法
除了直接使用API,SDK中同样提供了部分内联函数,消除处理数据结构的代码冗余:
inline fun <T> LazyListScope.items(
items: List<T>,
noinline key: ((item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
)
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
)
inline fun <T> LazyListScope.items(
items: Array<T>,
noinline key: ((item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
)
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
)
按照以往Android中的开发经验,我们很容易写出如下的代码:
// WorkShop 中的入口页面,枚举了各个例子对应的Activity
@Composable
fun TestList(activity: Activity, cases: List<Pair<String, Class<out Activity>>>) {
LazyColumn(contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)) {
itemsIndexed(items = cases) { _, item ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.size(3.dp))
Box(
modifier = Modifier
.height(48.dp)
.fillMaxWidth()
.background(
color = Color.LightGray,
shape = RoundedCornerShape(CornerSize(6.dp))
)
.clickable {
activity.startActivity(Intent(activity, item.second))
},
contentAlignment = Alignment.Center
) {
Text(text = item.first, color = MainTxt, textAlign = TextAlign.Center)
}
Spacer(modifier = Modifier.size(3.dp))
}
}
}
}
TestList(
activity = this@MainActivity, cases = arrayListOf(
"Layout samples" to P21LayoutSample::class.java,
"Draw samples" to P21DrawSample::class.java,
"Text samples" to P26TextSample::class.java,
"TextField samples" to P26TextFieldSample::class.java,
"Button samples" to P26ButtonSample::class.java,
"Icon samples" to P27IconSample::class.java,
"Image samples" to P27ImageSample::class.java,
"Switch,Checkbox,RadioButton samples" to P28SwitchRbCbSample::class.java,
"Box,Row,Column samples" to P29BoxRowColumnSample::class.java,
)
)
如果从Android的视角出发,这段代码相当于创建 ViewHolder的ItemView 以及 onBindViewHolder 的实现
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.size(3.dp))
Box(
modifier = Modifier
.height(48.dp)
.fillMaxWidth()
.background(
color = Color.LightGray,
shape = RoundedCornerShape(CornerSize(6.dp))
)
.clickable {
activity.startActivity(Intent(activity, item.second))
},
contentAlignment = Alignment.Center
) {
Text(text = item.first, color = MainTxt, textAlign = TextAlign.Center)
}
Spacer(modifier = Modifier.size(3.dp))
}
也就是说,我们利用了ItemView固有的 "留白" 处理了Item之间的间距,显然这不是最佳实践方案!
更加优雅地处理间距和对齐
上文中已经提及:
- contentPadding
- verticalArrangement
- horizontalAlignment
基于此我们对代码进行改造,以减少没用的嵌套
@Composable
fun TestList(activity: Activity, cases: List<Pair<String, Class<out Activity>>>) {
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = spacedBy(6.dp, Alignment.Top),
horizontalAlignment = Alignment.CenterHorizontally,
) {
itemsIndexed(items = cases) { _, item ->
Box(
modifier = Modifier
.height(48.dp)
.fillMaxWidth()
.background(
color = Color.LightGray,
shape = RoundedCornerShape(CornerSize(6.dp))
)
.clickable {
activity.startActivity(Intent(activity, item.second))
},
contentAlignment = Alignment.Center
) {
Text(
text = item.first,
color = MainTxt,
textAlign = TextAlign.Center
)
}
}
}
}
可以得到一致的效果:
让Column可滑动
参考可以实现Row的可滑动
最开始在和Modifier混脸熟的过程中,我们提及了 androidx.compose.foundation
包,并且含有子包: androidx.compose.foundation.gestures
,顾名思义,后者和手势处理有关。
- androidx.compose.foundation.gestures.ScrollableKt#scrollable
- androidx.compose.foundation.ScrollKt#verticalScroll
- androidx.compose.foundation.ScrollKt#horizontalScroll
经过这一阶段的学习,我们可以做出一个结论:
Compose 中包含一部分基本的函数,以及结合实际使用场景,在基本函数上 "装饰" 出高级函数
从命名上,我们很容易得知 scrollable
是比较基本的函数,verticalScroll
,horizontalScroll
是基于 scrollable
装饰出的高级函数。
阅读源码后也确实验证了我们的推测。
以 verticalScroll 为例,horizontalScroll暂不展开
Doc内容如下:
Modify element to allow to scroll vertically when height of the content is bigger than max constraints allow. In order to use this modifier, you need to create and own [ScrollState] @see [rememberScrollState]
修饰布局元素,当其内容高度超过最大允许的限制时,允许在纵向进行滚动。
注:内容最大高度地限制需要考虑容器的高度、padding、offset等内容
为了使用它,你需要创建并持有 ScrollState
实例,参见 rememberScrollState
方法原型:
fun Modifier.verticalScroll(
state: ScrollState,
enabled: Boolean = true,
flingBehavior: FlingBehavior? = null,
reverseScrolling: Boolean = false
)
- state:滚动状态,ScrollState实例
- enabled:当触摸事件发生时,是否允许滑动
- flingBehavior:fling处理逻辑
- reverseScrolling:是否反向滑动
Demo
Column(
modifier = Modifier
.fillMaxWidth()
.height(600.dp)
.verticalScroll(
state = rememberScrollState()
)
) {
Box(
Modifier
.fillMaxWidth()
.height(400.dp)
.background(Color.Green)
)
Spacer(modifier = Modifier.height(50.dp))
Box(
Modifier
.fillMaxWidth()
.height(400.dp)
.background(Color.Blue)
)
}
很显然,内容高度已经超过了最大限制!