Jetpack Compose | Compose 滑动列表真的需要使用LazyColumn吗?No No No!

Posted leobert_lan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
                )
            }
        }
    }
}

可以得到一致的效果:

让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)
    )

}

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

效果

结语

至此,Compose中的列表我们已经学习完成,经过简单探索Modifier中滑动的相关内容,掌握了让Column在内容超出展示限制时可以响应滑动的方案。
较为遗憾的是,目前我们所掌握的知识还不足以支撑我们继续探索下去。

当然,在后续的文章中,我们会继续学习相关的基础知识,最终一起探索Compose中深层次的内容

另:最近祖传代码改的有点精神分裂,文章更新效率还是没上的来。

读者朋友们,如果觉得我的分享对你有一些帮助,还请点个赞让我知道,给予我支持下去的动力;如果内容写的不好,也烦请留个言把想看的内容告知我,可以在后续的文章中一起交流!

以上是关于Jetpack Compose | Compose 滑动列表真的需要使用LazyColumn吗?No No No!的主要内容,如果未能解决你的问题,请参考以下文章

Android Jetpack Compose学习—— Jetpack compose基础布局

Android Jetpack Compose学习—— Jetpack compose基础布局

Android Jetpack Compose学习—— Jetpack compose基础布局

Android Jetpack Compose学习—— Jetpack compose入门

Android Jetpack Compose学习—— Jetpack compose入门

什么是Jetpack Compose?带你走进Jetpack Compose~