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实现上拉丝滑加载和下拉刷新

相关推荐
枯骨成佛39 分钟前
Android中Crash Debug技巧
android
kim56596 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼6 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ6 小时前
Android Studio使用c++编写
android·c++
csucoderlee7 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio
kim56597 小时前
各版本android studio下载地址
android·ide·android studio
饮啦冰美式7 小时前
Android Studio 将项目打包成apk文件
android·ide·android studio
夜色。7 小时前
Unity6 + Android Studio 开发环境搭建【备忘】
android·unity·android studio
ROCKY_8178 小时前
AndroidStudio-滚动视图ScrollView
android
趴菜小玩家9 小时前
使用 Gradle 插件优化 Flutter Android 插件开发中的 Flutter 依赖缺失问题
android·flutter·gradle