注册

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,例如:paddingoffset,但并无 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
)
}
}
}
}

可以得到一致的效果:

p30_demo1.webp

让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 是比较基本的函数,verticalScrollhorizontalScroll 是基于 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)
)

}

很显然,内容高度已经超过了最大限制!

0 个评论

要回复文章请先登录注册