Compose 的下拉刷新有现成的 Material 库能够直接运用,十分简略便利。
但是上拉加载现在没看到有封装的特别好的库,Paging 有些场景无法满足,而且上拉加载也是个比较简略的功用,没必要再去依靠一个质量不知道的库。咱们能够根据现在的 LazyList 简略的封装一个灵敏的组件。
基本原则是仍然根据现有的 PullRefresh 以及 LazyList API 完成,不依靠三方库,运用简略灵敏好用。
接口规划
首先咱们将这个能够上拉加载下拉刷新的 Compose 函数命名为 LoadableLazyColumn
。
上面说到咱们需求根据 PullRefresh 以及 LazyList API 完成,这两个组件都具备各自的 State。
-
PullRefreshState
:下拉刷新的 State -
LazyListState
:LazyColumn 的 State
由于咱们需求在此基础上供给上拉加载的能力,那还需求一个上拉加载的 State,咱们能够将其命名为 LoadMoreState
,现在 LoadMoreState
需求包含两个参数:
-
loadMoreRemainCountThreshold
:加载更多的剩下 Item 个数阈值,当剩下个数小于等于这个阈值时开端发起加载更多请求。 -
onLoadMore
:加载更多的工作回调。
已然供给了 LoadMoreState
,咱们还应该供给一个对应的 remember 函数。
@Composable
fun rememberLoadMoreState(
loadMoreRemainCountThreshold: Int,
onLoadMore: () -> Unit,
): LoadMoreState {
return remember {
LoadMoreState(loadMoreRemainCountThreshold, onLoadMore)
}
}
上面咱们仅仅单纯的定义了 LoadMoreState
,一起咱们也知道了 LoadableLazyColumn
还包含别的两个 State,一共也便是三个 State。
现在咱们需求创建 LoadableLazyColumnState
,它需求包含上面说的三个 State。
@OptIn(ExperimentalMaterialApi::class)
data class LoadableLazyColumnState(
val lazyListState: LazyListState,
val pullRefreshState: PullRefreshState,
val loadMoreState: LoadMoreState,
)
以及对应的 remember
办法。
不过上面说的三个 state 仅仅咱们的内部完成,这不是调用者需求考虑的工作,关于运用者来说这仅仅一个 state,因而咱们的 remember
办法的参数应该是这三个 state 的合集。
@Composable
@ExperimentalMaterialApi
fun rememberLoadableLazyColumnState(
refreshing: Boolean,
onRefresh: () -> Unit,
onLoadMore: () -> Unit,
refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
loadMoreRemainCountThreshold: Int = 5,
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Int = 0
): LoadableLazyColumnState {
val pullRefreshState = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = onRefresh,
refreshingOffset = refreshingOffset,
refreshThreshold = refreshThreshold,
)
val lazyListState = rememberLazyListState(
initialFirstVisibleItemScrollOffset = initialFirstVisibleItemScrollOffset,
initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,
)
val loadMoreState = rememberLoadMoreState(loadMoreRemainCountThreshold, onLoadMore)
return remember(pullRefreshState, lazyListState, loadMoreState) {
LoadableLazyColumnState(
lazyListState = lazyListState,
pullRefreshState = pullRefreshState,
loadMoreState = loadMoreState,
)
}
}
这样咱们就创建了 LoadableLazyColumnState
。
然后 LoadableLazyColumn
这个函数的入参就清楚明了了。
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun LoadableLazyColumn(
modifier: Modifier = Modifier,
state: LoadableLazyColumnState,
refreshing: Boolean,
loading: Boolean,
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(),
userScrollEnabled: Boolean = true,
loadingContent: (@Composable () -> Unit)? = null,
content: LazyListScope.() -> Unit,
)
完成方案
这儿会根据 LazyList
滑动工作来触发加载更多工作,当滑动工作完毕后,判别用户是否为向下滑动,而且剩下元素的个数小于等于设定的阈值。
所幸 lazyListState
供给了这些状况,咱们能够经过它那计算出上面的情况。
val lazyListState = state.lazyListState
// 获取 lazyList 布局信息
val listLayoutInfo by remember { derivedStateOf { lazyListState.layoutInfo } }
能够经过下面的办法获取到 LazyList 是否正在滑动:
// Whether this [ScrollableState] is currently scrolling by gesture,
// fling or programmatically ornot.
lazyListState.isScrollInProgress
然后经过下面的两个办法获取到最终一个可见的 index
,以及 item
总数:
listLayoutInfo.visibleItemsInfo.lastOrNull()?.index
listLayoutInfo.totalItemsCount
上面说的几个办法都是获取当时状况,但咱们的目的是判别状况的改变,主要是下面两个工作改变:
- 滑动中止工作
- 最终一个可见 index 改变工作
如果咱们能在滑动工作中止后判别最终一个可见 index 与前次滑动完毕后的最终一个可见 index 相比的巨细,就知道是向上滑动仍是向下滑动了。再加上最终一个可见 index 与阈值相比,就能够判别触发加载更多工作了。
这儿咱们运用 remember
函数来完成,即 remember
前次的值,与当时值做比照。
// 前次是否正在滑动
var lastTimeIsScrollInProgress by remember {
mutableStateOf(lazyListState.isScrollInProgress)
}
// 前次滑动完毕后最终一个可见的index
var lastTimeLastVisibleIndex by remember {
mutableStateOf(listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0)
}
// 当时是否正在滑动
val currentIsScrollInProgress = lazyListState.isScrollInProgress
// 当时最终一个可见的 index
val currentLastVisibleIndex = listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
经过上面的代码咱们就拿到了所有需求的状况了,然后简略比照一下即可。
if (!currentIsScrollInProgress && lastTimeIsScrollInProgress) {
if (currentLastVisibleIndex != lastTimeLastVisibleIndex) {
val isScrollDown = currentLastVisibleIndex > lastTimeLastVisibleIndex
val remainCount = listLayoutInfo.totalItemsCount - currentLastVisibleIndex - 1
if (isScrollDown && remainCount <= state.loadMoreState.loadMoreRemainCountThreshold) {
LaunchedEffect(Unit) {
state.loadMoreState.onLoadMore()
}
}
}
// 滑动完毕后再更新值
lastTimeLastVisibleIndex = currentLastVisibleIndex
}
lastTimeIsScrollInProgress = currentIsScrollInProgress
这样就差不多了,看下所有的代码。
package com.zhangke.framework.loadable.lazycolumn
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshDefaults
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun LoadableLazyColumn(
modifier: Modifier = Modifier,
state: LoadableLazyColumnState,
refreshing: Boolean,
loading: Boolean,
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(),
userScrollEnabled: Boolean = true,
loadingContent: (@Composable () -> Unit)? = null,
content: LazyListScope.() -> Unit,
) {
val lazyListState = state.lazyListState
// 获取 lazyList 布局信息
val listLayoutInfo by remember { derivedStateOf { lazyListState.layoutInfo } }
Box(
modifier = modifier
.pullRefresh(state.pullRefreshState)
) {
LazyColumn(
contentPadding = contentPadding,
state = state.lazyListState,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
content = {
content()
item {
if (loadingContent != null) {
loadingContent()
} else {
if (loading) {
Box(modifier = Modifier.fillMaxWidth()) {
CircularProgressIndicator(
modifier = Modifier
.size(30.dp)
.align(Alignment.Center)
)
}
}
}
}
},
)
PullRefreshIndicator(
refreshing,
state.pullRefreshState,
Modifier.align(Alignment.TopCenter)
)
}
// 前次是否正在滑动
var lastTimeIsScrollInProgress by remember {
mutableStateOf(lazyListState.isScrollInProgress)
}
// 前次滑动完毕后最终一个可见的index
var lastTimeLastVisibleIndex by remember {
mutableStateOf(listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0)
}
// 当时是否正在滑动
val currentIsScrollInProgress = lazyListState.isScrollInProgress
// 当时最终一个可见的 index
val currentLastVisibleIndex = listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
if (!currentIsScrollInProgress && lastTimeIsScrollInProgress) {
if (currentLastVisibleIndex != lastTimeLastVisibleIndex) {
val isScrollDown = currentLastVisibleIndex > lastTimeLastVisibleIndex
val remainCount = listLayoutInfo.totalItemsCount - currentLastVisibleIndex - 1
if (isScrollDown && remainCount <= state.loadMoreState.loadMoreRemainCountThreshold) {
LaunchedEffect(Unit) {
state.loadMoreState.onLoadMore()
}
}
}
// 滑动完毕后再更新值
lastTimeLastVisibleIndex = currentLastVisibleIndex
}
lastTimeIsScrollInProgress = currentIsScrollInProgress
}
@Composable
@ExperimentalMaterialApi
fun rememberLoadableLazyColumnState(
refreshing: Boolean,
onRefresh: () -> Unit,
onLoadMore: () -> Unit,
refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
loadMoreRemainCountThreshold: Int = 5,
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Int = 0
): LoadableLazyColumnState {
val pullRefreshState = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = onRefresh,
refreshingOffset = refreshingOffset,
refreshThreshold = refreshThreshold,
)
val lazyListState = rememberLazyListState(
initialFirstVisibleItemScrollOffset = initialFirstVisibleItemScrollOffset,
initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,
)
val loadMoreState = rememberLoadMoreState(loadMoreRemainCountThreshold, onLoadMore)
return remember(pullRefreshState, lazyListState, loadMoreState) {
LoadableLazyColumnState(
lazyListState = lazyListState,
pullRefreshState = pullRefreshState,
loadMoreState = loadMoreState,
)
}
}
@Composable
fun rememberLoadMoreState(
loadMoreRemainCountThreshold: Int,
onLoadMore: () -> Unit,
): LoadMoreState {
return remember {
LoadMoreState(loadMoreRemainCountThreshold, onLoadMore)
}
}
data class LoadMoreState(
val loadMoreRemainCountThreshold: Int,
val onLoadMore: () -> Unit,
)
@OptIn(ExperimentalMaterialApi::class)
data class LoadableLazyColumnState(
val lazyListState: LazyListState,
val pullRefreshState: PullRefreshState,
val loadMoreState: LoadMoreState,
)
这便是所有代码了。