一、使用verticalScroll/horizontalScroll实现滚动
在XML中实现超出屏幕的滚动一般使用ScrollView
,在Compose
中一般通过给Column
的Modifier
添加verticalScroll()
方法或通过给Row
的Modifier
添加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
给Column
的Modifier
添加verticalScroll()
方法可以让列表实现滑动。但是如果列表过长,众多的内容会占用大量的内存。然而更多的内容对于用户其实都是不可见的,没必要加载到内存。所以Compose
提供了专门用于处理长列表的组件,这些组件只会在我们能看到的列表部分进行重组和布局,它们分别是LazyColumn
和LazyRow
。其作用类似于传统视图中的ListView
或者RecyclerView
。
1、LazyListScope作用域
LazyColumn
和LazyRow
内部都是基于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效果,如下:
这种写法确实很丝滑,但一般我们实际开发中更倾向于传统带视图的上拉加载形式。