feat: 列表滑动预加载

This commit is contained in:
HuanCheng65 2023-09-22 17:14:22 +08:00
parent 4f3538339e
commit 1e99d0fe21
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
9 changed files with 75 additions and 18 deletions

View File

@ -347,7 +347,9 @@ fun ForumThreadListPage(
) )
) )
}, },
loadEnd = !hasMore loadEnd = !hasMore,
lazyListState = lazyListState,
isEmpty = threadList.isEmpty(),
) { ) {
ThreadList( ThreadList(
state = lazyListState, state = lazyListState,

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.DropdownMenuItem import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -94,6 +95,7 @@ fun HistoryListPage(
viewModel.onEvent<HistoryListUiEvent.Delete.Success> { viewModel.onEvent<HistoryListUiEvent.Delete.Success> {
snackbarHostState.showSnackbar(context.getString(R.string.delete_history_success)) snackbarHostState.showSnackbar(context.getString(R.string.delete_history_success))
} }
val lazyListState = rememberLazyListState()
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -101,9 +103,15 @@ fun HistoryListPage(
LoadMoreLayout( LoadMoreLayout(
isLoading = isLoadingMore, isLoading = isLoadingMore,
onLoadMore = { viewModel.send(HistoryListUiIntent.LoadMore(currentPage + 1)) }, onLoadMore = { viewModel.send(HistoryListUiIntent.LoadMore(currentPage + 1)) },
loadEnd = !hasMore loadEnd = !hasMore,
lazyListState = lazyListState,
isEmpty = todayHistoryData.isEmpty() && beforeHistoryData.isEmpty()
) { ) {
LazyColumn { LazyColumn(
modifier = Modifier
.fillMaxSize(),
state = lazyListState
) {
if (todayHistoryData.isNotEmpty()) { if (todayHistoryData.isNotEmpty()) {
stickyHeader(key = "TodayHistoryHeader") { stickyHeader(key = "TodayHistoryHeader") {
Column( Column(

View File

@ -78,7 +78,8 @@ fun ConcernPage(
) { ) {
LoadMoreLayout( LoadMoreLayout(
isLoading = isLoadingMore, isLoading = isLoadingMore,
onLoadMore = { viewModel.send(ConcernUiIntent.LoadMore(nextPageTag)) } onLoadMore = { viewModel.send(ConcernUiIntent.LoadMore(nextPageTag)) },
lazyListState = lazyListState,
) { ) {
val windowSizeClass = BaseComposeActivity.LocalWindowSizeClass.current val windowSizeClass = BaseComposeActivity.LocalWindowSizeClass.current
val itemFraction = when (windowSizeClass.widthSizeClass) { val itemFraction = when (windowSizeClass.widthSizeClass) {

View File

@ -36,6 +36,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
@ -113,7 +114,7 @@ fun PersonalizedPage(
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
viewModel.bindScrollToTopEvent(lazyListState = lazyListState) viewModel.bindScrollToTopEvent(lazyListState = lazyListState)
var refreshCount by remember { var refreshCount by remember {
mutableStateOf(0) mutableIntStateOf(0)
} }
var showRefreshTip by remember { var showRefreshTip by remember {
mutableStateOf(false) mutableStateOf(false)
@ -150,8 +151,10 @@ fun PersonalizedPage(
Box(modifier = Modifier.pullRefresh(pullRefreshState)) { Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
LoadMoreLayout( LoadMoreLayout(
isLoading = isLoadingMore, isLoading = isLoadingMore,
loadEnd = false,
onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) }, onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) },
loadEnd = false,
lazyListState = lazyListState,
isEmpty = data.isEmpty()
) { ) {
FeedList( FeedList(
state = lazyListState, state = lazyListState,

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text import androidx.compose.material.Text
@ -80,17 +81,21 @@ fun NotificationsListPage(
) )
val pullRefreshState = rememberPullRefreshState( val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing, refreshing = isRefreshing,
onRefresh = { viewModel.send(NotificationsListUiIntent.Refresh) }) onRefresh = { viewModel.send(NotificationsListUiIntent.Refresh) }
)
val lazyListState = rememberLazyListState()
Box( Box(
modifier = Modifier.pullRefresh(pullRefreshState) modifier = Modifier.pullRefresh(pullRefreshState)
) { ) {
LoadMoreLayout( LoadMoreLayout(
isLoading = isLoadingMore, isLoading = isLoadingMore,
loadEnd = !hasMore,
onLoadMore = { viewModel.send(NotificationsListUiIntent.LoadMore(currentPage + 1)) }, onLoadMore = { viewModel.send(NotificationsListUiIntent.LoadMore(currentPage + 1)) },
loadEnd = !hasMore,
lazyListState = lazyListState,
) { ) {
LazyColumn( LazyColumn(
contentPadding = PaddingValues(vertical = 4.dp) contentPadding = PaddingValues(vertical = 4.dp),
state = lazyListState,
) { ) {
items( items(
items = data, items = data,

View File

@ -354,7 +354,6 @@ internal fun SubPostsContent(
) { paddingValues -> ) { paddingValues ->
LoadMoreLayout( LoadMoreLayout(
isLoading = isLoading, isLoading = isLoading,
loadEnd = !hasMore,
onLoadMore = { onLoadMore = {
viewModel.send( viewModel.send(
SubPostsUiIntent.LoadMore( SubPostsUiIntent.LoadMore(
@ -365,7 +364,10 @@ internal fun SubPostsContent(
currentPage + 1, currentPage + 1,
) )
) )
} },
loadEnd = !hasMore,
lazyListState = lazyListState,
isEmpty = post == null && subPosts.isEmpty(),
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),

View File

@ -1114,7 +1114,6 @@ fun ThreadPage(
) { ) {
LoadMoreLayout( LoadMoreLayout(
isLoading = isLoadingMore, isLoading = isLoadingMore,
loadEnd = !hasMore,
onLoadMore = { onLoadMore = {
viewModel.send( viewModel.send(
ThreadUiIntent.LoadMore( ThreadUiIntent.LoadMore(
@ -1128,7 +1127,10 @@ fun ThreadPage(
postIds = data.map { it.post.get { id } } postIds = data.map { it.post.get { id } }
) )
) )
} },
loadEnd = !hasMore,
lazyListState = lazyListState,
isEmpty = firstPost == null && data.isEmpty()
) { ) {
LazyColumn( LazyColumn(
state = lazyListState, state = lazyListState,

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
@ -157,13 +158,16 @@ fun ThreadStorePage(
onRefresh = { viewModel.send(ThreadStoreUiIntent.Refresh) } onRefresh = { viewModel.send(ThreadStoreUiIntent.Refresh) }
) )
val lazyListState = rememberLazyListState()
Box(modifier = Modifier.pullRefresh(pullRefreshState)) { Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
LoadMoreLayout( LoadMoreLayout(
isLoading = isLoadingMore, isLoading = isLoadingMore,
onLoadMore = { viewModel.send(ThreadStoreUiIntent.LoadMore(currentPage + 1)) }, onLoadMore = { viewModel.send(ThreadStoreUiIntent.LoadMore(currentPage + 1)) },
loadEnd = !hasMore loadEnd = !hasMore,
lazyListState = lazyListState
) { ) {
LazyColumn { LazyColumn(state = lazyListState) {
items( items(
items = data, items = data,
key = { it.threadId } key = { it.threadId }

View File

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
@ -23,11 +24,13 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -45,6 +48,7 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.loadMoreIndicator
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.abs import kotlin.math.abs
@ -66,12 +70,15 @@ fun LoadMoreLayout(
willLoad = willLoad willLoad = willLoad
) )
}, },
content: @Composable () -> Unit lazyListState: LazyListState? = null,
isEmpty: Boolean = lazyListState?.layoutInfo?.totalItemsCount == 0,
preloadCount: Int = 1,
content: @Composable () -> Unit,
) { ) {
val loadDistance = with(LocalDensity.current) { LoadDistance.toPx() } val loadDistance = with(LocalDensity.current) { LoadDistance.toPx() }
val curOnLoadMore by rememberUpdatedState(newValue = onLoadMore) val curOnLoadMore by rememberUpdatedState(newValue = onLoadMore)
var lastTriggerTime by remember { mutableStateOf(0L) } var lastTriggerTime by remember { mutableLongStateOf(0L) }
var waitingStateReset by remember { mutableStateOf(false) } var waitingStateReset by remember { mutableStateOf(false) }
val loadMoreFlow = remember { val loadMoreFlow = remember {
MutableSharedFlow<Long>( MutableSharedFlow<Long>(
@ -95,9 +102,32 @@ fun LoadMoreLayout(
} }
val canLoadMore = remember(enableLoadMore, loadEnd) { enableLoadMore && !loadEnd } val canLoadMore = remember(enableLoadMore, loadEnd) { enableLoadMore && !loadEnd }
val curIsEmpty by rememberUpdatedState(newValue = isEmpty)
val curIsLoading by rememberUpdatedState(newValue = isLoading) val curIsLoading by rememberUpdatedState(newValue = isLoading)
val curCanLoadMore by rememberUpdatedState(newValue = canLoadMore) val curCanLoadMore by rememberUpdatedState(newValue = canLoadMore)
// 处理列表滚动到底部时自动加载更多
val curLazyListState by rememberUpdatedState(newValue = lazyListState)
LaunchedEffect(curLazyListState) {
curLazyListState?.let { state ->
snapshotFlow {
val shouldPreload = !curIsEmpty && curCanLoadMore && !curIsLoading
val isInPreloadRange =
state.firstVisibleItemIndex + state.layoutInfo.visibleItemsInfo.size - 1 >= state.layoutInfo.totalItemsCount - preloadCount
shouldPreload && isInPreloadRange
}
.distinctUntilChanged()
.collect {
if (it) {
val curTime = System.currentTimeMillis()
coroutineScope.launch {
loadMoreFlow.emit(curTime)
}
curTime - lastTriggerTime >= 500
}
}
}
}
val swipeableState = rememberSwipeableState(false) { newValue -> val swipeableState = rememberSwipeableState(false) { newValue ->
if (newValue && !curIsLoading && curCanLoadMore) { if (newValue && !curIsLoading && curCanLoadMore) {
@ -116,7 +146,7 @@ fun LoadMoreLayout(
} }
} }
LaunchedEffect(isLoading) { swipeableState.animateTo(isLoading) } LaunchedEffect(isLoading) { if (!isLoading) swipeableState.animateTo(isLoading) }
Box( Box(
modifier = Modifier modifier = Modifier