關(guān)注個(gè)人簡(jiǎn)介,技術(shù)不迷路
Compose 的下拉刷新有現(xiàn)成的 Material 庫(kù)可以直接使用,非常簡(jiǎn)單方便。
但是上拉加載目前沒(méi)看到有封裝的特別好的庫(kù),Paging 有些場(chǎng)景無(wú)法滿足,而且上拉加載也是個(gè)比較簡(jiǎn)單的功能,沒(méi)必要再去依賴一個(gè)質(zhì)量未知的庫(kù)。我們可以基于目前的 LazyList 簡(jiǎn)單的封裝一個(gè)靈活的組件。
基本原則是仍然基于現(xiàn)有的 PullRefresh 以及 LazyList API 實(shí)現(xiàn),不依賴三方庫(kù),使用簡(jiǎn)單靈活好用。
接口設(shè)計(jì)
首先我們將這個(gè)可以上拉加載下拉刷新的 Compose 函數(shù)命名為 LoadableLazyColumn。
上面說(shuō)到我們需要基于 PullRefresh 以及 LazyList API 實(shí)現(xiàn),這兩個(gè)組件都具備各自的 State。
-
PullRefreshState:下拉刷新的 State -
LazyListState:LazyColumn 的 State
由于我們需要在此基礎(chǔ)上提供上拉加載的能力,那還需要一個(gè)上拉加載的 State,我們可以將其命名為 LoadMoreState ,目前 LoadMoreState 需要包含兩個(gè)參數(shù):
-
loadMoreRemainCountThreshold:加載更多的剩余 Item 個(gè)數(shù)閾值,當(dāng)剩余個(gè)數(shù)小于等于這個(gè)閾值時(shí)開始發(fā)起加載更多請(qǐng)求。 -
onLoadMore:加載更多的事件回調(diào)。
既然提供了 LoadMoreState ,我們還應(yīng)該提供一個(gè)對(duì)應(yīng)的 remember 函數(shù)。
@Composable
fun rememberLoadMoreState(
loadMoreRemainCountThreshold: Int,
onLoadMore: () -> Unit,
): LoadMoreState {
return remember {
LoadMoreState(loadMoreRemainCountThreshold, onLoadMore)
}
}
上面我們只是單純的定義了 LoadMoreState,同時(shí)我們也知道了 LoadableLazyColumn 還包含另外兩個(gè) State,總共也就是三個(gè) State。
現(xiàn)在我們需要?jiǎng)?chuàng)建 LoadableLazyColumnState ,它需要包含上面說(shuō)的三個(gè) State。
@OptIn(ExperimentalMaterialApi::class)
data class LoadableLazyColumnState(
val lazyListState: LazyListState,
val pullRefreshState: PullRefreshState,
val loadMoreState: LoadMoreState,
)
以及對(duì)應(yīng)的 remember 方法。
不過(guò)上面說(shuō)的三個(gè) state 只是我們的內(nèi)部實(shí)現(xiàn),這不是調(diào)用者需要考慮的事情,對(duì)于使用者來(lái)說(shuō)這只是一個(gè) state,因此我們的 remember 方法的參數(shù)應(yīng)該是這三個(gè) 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 {
LoadableLazyColumnState(
lazyListState = lazyListState,
pullRefreshState = pullRefreshState,
loadMoreState = loadMoreState,
)
}
}
這樣我們就創(chuàng)建了 LoadableLazyColumnState。
然后 LoadableLazyColumn 這個(gè)函數(shù)的入?yún)⒕惋@而易見了。
@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,
)
實(shí)現(xiàn)方案
這里會(huì)根據(jù) LazyList 滑動(dòng)事件來(lái)觸發(fā)加載更多事件,當(dāng)滑動(dòng)事件結(jié)束后,判斷用戶是否為向下滑動(dòng),并且剩余元素的個(gè)數(shù)小于等于設(shè)定的閾值。
所幸 lazyListState 提供了這些狀態(tài),我們可以通過(guò)它那計(jì)算出上面的情況。
val lazyListState = state.lazyListState
// 獲取 lazyList 布局信息
val listLayoutInfo by remember { derivedStateOf { lazyListState.layoutInfo } }
可以通過(guò)下面的方法獲取到 LazyList 是否正在滑動(dòng):
// Whether this [ScrollableState] is currently scrolling by gesture,
// fling or programmatically ornot.
lazyListState.isScrollInProgress
然后通過(guò)下面的兩個(gè)方法獲取到最后一個(gè)可見的 index,以及 item 總數(shù):
listLayoutInfo.visibleItemsInfo.lastOrNull()?.index
listLayoutInfo.totalItemsCount
上面說(shuō)的幾個(gè)方法都是獲取當(dāng)前狀態(tài),但我們的目的是判斷狀態(tài)的變化,主要是下面兩個(gè)事件變化:
- 滑動(dòng)停止事件
- 最后一個(gè)可見 index 變化事件
如果我們能在滑動(dòng)事件停止后判斷最后一個(gè)可見 index 與上次滑動(dòng)結(jié)束后的最后一個(gè)可見 index 相比的大小,就知道是向上滑動(dòng)還是向下滑動(dòng)了。再加上最后一個(gè)可見 index 與閾值相比,就可以判斷觸發(fā)加載更多事件了。
這里我們使用 remember 函數(shù)來(lái)實(shí)現(xiàn),即 remember 上次的值,與當(dāng)前值做對(duì)比。
// 上次是否正在滑動(dòng)
var lastTimeIsScrollInProgress by remember {
mutableStateOf(lazyListState.isScrollInProgress)
}
// 上次滑動(dòng)結(jié)束后最后一個(gè)可見的index
var lastTimeLastVisibleIndex by remember {
mutableStateOf(listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0)
}
// 當(dāng)前是否正在滑動(dòng)
val currentIsScrollInProgress = lazyListState.isScrollInProgress
// 當(dāng)前最后一個(gè)可見的 index
val currentLastVisibleIndex = listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
通過(guò)上面的代碼我們就拿到了所有需要的狀態(tài)了,然后簡(jiǎn)單對(duì)比一下即可。
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()
}
}
}
// 滑動(dòng)結(jié)束后再更新值
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)
)
}
// 上次是否正在滑動(dòng)
var lastTimeIsScrollInProgress by remember {
mutableStateOf(lazyListState.isScrollInProgress)
}
// 上次滑動(dòng)結(jié)束后最后一個(gè)可見的index
var lastTimeLastVisibleIndex by remember {
mutableStateOf(listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0)
}
// 當(dāng)前是否正在滑動(dòng)
val currentIsScrollInProgress = lazyListState.isScrollInProgress
// 當(dāng)前最后一個(gè)可見的 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()
}
}
}
// 滑動(dòng)結(jié)束后再更新值
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 {
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,
)
這就是所有代碼了。