Jetpack Compose(九)-列表和下拉刷新上拉加载

一、使用verticalScroll/horizontalScroll实现滚动

在XML中实现超出屏幕的滚动一般使用ScrollView,在Compose中一般通过给ColumnModifier添加verticalScroll()方法或通过给RowModifier添加horizontalScroll()方法来让多个子控件实现滚动,下面是一个竖向滚动例子,横向滚动类型:

kotlin 复制代码
@Composable
fun Greeting() {
    val items = listOf("语文", "数学", "物理", "化学")
    val scrollState = rememberScrollState()

    Column(
        modifier = Modifier
            .height(200.dp)    //父布局限定高度
            .fillMaxWidth()
            .background(Color.LightGray)
            .verticalScroll(scrollState)      //垂直滚动
    ) {
        items.forEachIndexed { index, itemText ->
            Box(
                modifier = Modifier
                    .height(80.dp)
                    .fillMaxWidth()
                    .background(Color.Cyan),
                contentAlignment = Alignment.Center
            ) {
                Text(text = itemText)
            }
            Spacer(modifier = Modifier.height(1.dp))
        }
    }
}

UI效果

二、LazyComposables

ColumnModifier添加verticalScroll()方法可以让列表实现滑动。但是如果列表过长,众多的内容会占用大量的内存。然而更多的内容对于用户其实都是不可见的,没必要加载到内存。所以Compose提供了专门用于处理长列表的组件,这些组件只会在我们能看到的列表部分进行重组和布局,它们分别是LazyColumnLazyRow。其作用类似于传统视图中的ListView或者RecyclerView

1、LazyListScope作用域

LazyColumnLazyRow内部都是基于LazyList组件实现的,虽然这是一个internal的内部组件,我们无法直接使用它。LazyList和其他布局类组件不同,不能在它的content里面直接裸写子Composable组件。它的content是一个LazyListScope.()-> Unit类型的作用域代码块,在内部通过LazyListScope提供的item等方法来描述列表中的内容,整体符合DSL的代码风格,下面是例子:

kotlin 复制代码
data class Subjects(val name:String)

@Composable
fun Greeting() {
    val itemList = listOf("语文", "数学", "物理", "化学", "美术")
    val subjectsList = itemList.map {
        Subjects(it)
    }
    LazyColumn {
        //创建1个条目
        item {
            Text(text = itemList[0])
        }

        //再创建5个条目
        items(5) { index ->
            Text(text = "${itemList[1]} $index")
        }

        //LazyListScope的扩展函数
        //再创建几个条目
        items(subjectsList) { subject ->
            Text(text = "$subject")
        }

        //LazyListScope的扩展函数,可以获取到index
        //再创建几个条目
        itemsIndexed(subjectsList){ index,subject->
            Text(text = "$subject $index")
        }
    }
}

UI效果

2、设置间距

有的时候也需要在列表中为内容整体设置外边距,这也非常容易,Lazy组件提供了contentPadding参数,如果想设置条目间竖直方向的间距,Lazy组件提供了verticalArrangement参数,下面是一个例子:

kotlin 复制代码
@Composable
fun Greeting() {
    val itemList = listOf("语文", "数学", "物理", "化学", "美术")
    val subjectsList = itemList.map {
        Subjects(it)
    }
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()                   //整个屏幕
            .background(Color.LightGray),    //背景浅灰色
        contentPadding = PaddingValues(15.dp),   //内边距
        verticalArrangement = Arrangement.spacedBy(15.dp)  //条目间竖直方向的间距
    ) {
        //LazyListScope的扩展函数,可以获取到index
        itemsIndexed(subjectsList) { index, subject ->
            Box(
                modifier = Modifier
                    .fillMaxWidth()      //最大宽度
                    .height(50.dp)
                    .background(Color.Cyan),   //条目青色
                contentAlignment = Alignment.Center
            ) {
                Text(text = "$subject $index")
            }
        }
    }
}

UI效果

三、下拉刷新

Modifier.pullRefresh 可以用于下拉刷新的实现。它的参数如下:

kotlin 复制代码
@ExperimentalMaterialApi
fun Modifier.pullRefresh(
    state: PullRefreshState,     //状态被PullRefreshState保存和更新
    enabled: Boolean = true
) = inspectable(...){
    Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
}
kotlin 复制代码
@ExperimentalMaterialApi
fun Modifier.pullRefresh(
    onPull: (pullDelta: Float) -> Float,   //垂直方向的量,向下为正
    onRelease: suspend (flingVelocity: Float) -> Float,   //拖动被释放时的回调
    enabled: Boolean = true
) = inspectable(...) {
    Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
}

第一种是基于第二种实现的。相关联的这个 state(PullRefreshState) 自然也有对应的 remember 方法用于创建,函数如下:

kotlin 复制代码
@Composable
@ExperimentalMaterialApi
fun rememberPullRefreshState(
    refreshing: Boolean,      //是否正在刷新
    onRefresh: () -> Unit,    //刷新时的回调
    refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,   //若超过阀值(默认80dp),则放手后会触发onRefresh
    refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,   //刷新时指示器的底部位置(默认56dp)
): PullRefreshState {...}

下面是一个代码示例:

kotlin 复制代码
@Composable
fun Greeting() {
    SwipeToRefreshTest()
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeToRefreshTest(
    modifier: Modifier = Modifier
) {
    val list = remember {
        List(4) { "Item $it" }.toMutableStateList()
    }
    //是否正在刷新
    var refreshing by remember {
        mutableStateOf(false)
    }
    // 用协程模拟一个耗时加载
    val scope = rememberCoroutineScope()
    val state = rememberPullRefreshState(refreshing = refreshing, onRefresh = {
        scope.launch {
            refreshing = true
            //模拟延时数据加载
            delay(1000)
            //添加一条数据
            list += "Item ${list.size + 1}"
            refreshing = false
        }
    })
    Box(
        modifier = modifier
            .fillMaxSize()
            .pullRefresh(state)   //下拉刷新

    ) {
        LazyColumn(
            Modifier
                .fillMaxSize()
                .background(Color.LightGray),
            contentPadding = PaddingValues(15.dp),
            verticalArrangement = Arrangement.spacedBy(15.dp)
        ) {
            items(list) { item ->
                Box(              //item的布局
                    modifier =
                    Modifier
                        .fillMaxWidth()
                        .height(50.dp)
                        .background(Color.Cyan),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = item)
                }
            }
        }
        //基于Android的SwipeRefreshLayout创建的指示器
        PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
    }
}

UI效果

四、上拉加载

这里是使用Jetpack Compose的Paging库实现的,需要先导包:

kotlin 复制代码
implementation("androidx.paging:paging-compose:1.0.0-alpha18")

然后先定义网络请求和分页的逻辑,继承PagingSource代码如下:

kotlin 复制代码
class RequestDataSource : PagingSource<Int, String>() {

    override fun getRefreshKey(state: PagingState<Int, String>): Int? {
        // 根据preKey和nextKey中找到离anchorPosition最近页面的键值
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
        // 定义键值
        val currentKey = params.key ?: 0
        return try {
            //随便给的数据,这里应该写网络请求
            val list = List(10) { "item ${(params.key ?: 0) * 10 + it}" }
            //prevKey和nextKey标记页面
            LoadResult.Page(
                data = list,
                prevKey = if (currentKey == 0) null else currentKey - 1,
                nextKey =  currentKey + 1
            )
        } catch (exception: Exception) {
            //异常的处理
            LoadResult.Error(exception)
        }
    }
}

这种很多都是模板写法,还处于初学阶段,就不深入源码了。再然后就是定义我们的请求,例如下面的代码仓库:

kotlin 复制代码
class RequestRepository{

    fun testRequest(): Flow<PagingData<String>> {
        // 通过Pager.flow返回流对象
        return Pager(
            config = PagingConfig(pageSize = 10),   //每页10条
            pagingSourceFactory = {
                //网络请求和分页逻辑
                RequestDataSource()
            }
        ).flow
    }
}

具体代码调用:

kotlin 复制代码
@Composable
fun Greeting() {
    LoadMoreTest()
}

@SuppressLint("FlowOperatorInvokedInComposition")
@Composable
fun LoadMoreTest() {
    //这行代码应该写在ViewModel中, demo代码就直接写在Activity中了
    //数据源
    val requestList =
        RequestRepository().testRequest().cachedIn(lifecycleScope).collectAsLazyPagingItems()

    Box(
        modifier = Modifier
            .fillMaxSize()
    ) {
        LazyColumn(
            Modifier
                .fillMaxSize()
                .background(Color.LightGray),
            contentPadding = PaddingValues(15.dp),
            verticalArrangement = Arrangement.spacedBy(15.dp)
        ) {
            items(requestList.itemCount) { index ->    //加载数据源
                Box(
                    modifier =
                    Modifier
                        .fillMaxWidth()
                        .height(50.dp)
                        .background(Color.Cyan),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = requestList[index] ?: "")
                }
            }
        }
    }
}

最后看看UI效果,如下:

这种写法确实很丝滑,但一般我们实际开发中更倾向于传统带视图的上拉加载形式。

参考内容:

Jetpack Compose博物馆

实体书 Jetpack Compose从入门到实战

Jetpack-Compose-Playground

Compose中结合Paging实现上拉丝滑加载和下拉刷新

相关推荐
万能的小裴同学1 小时前
Android M3U8视频播放器
android·音视频
q***57742 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober2 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿3 小时前
关于ObjectAnimator
android
zhangphil4 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我5 小时前
从头写一个自己的app
android·前端·flutter
lichong9516 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
用户69371750013846 小时前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
火柴就是我6 小时前
NekoBoxForAndroid 编译libcore.aar
android
Kaede68 小时前
MySQL中如何使用命令行修改root密码
android·mysql·adb