fix: 保留书架滚动状态并避免空白闪烁

This commit is contained in:
HapeLee
2026-07-01 01:02:56 +08:00
parent b60c025473
commit 45c403fe79
4 changed files with 70 additions and 49 deletions
@@ -2,8 +2,6 @@ package io.legado.app.ui.main
import android.content.Intent
import android.os.Build
import android.os.SystemClock
import android.view.ViewConfiguration
import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibilityScope
@@ -197,26 +195,20 @@ fun MainScreen(
orientation = Orientation.Horizontal,
)
var bookshelfScrollToTopRequest by remember { mutableLongStateOf(0L) }
var lastBookshelfNavClickTime by remember { mutableLongStateOf(0L) }
fun requestBookshelfScrollToTop(index: Int) {
var isBookshelfAtTop by remember { mutableStateOf(true) }
fun requestBookshelfScrollToTop() {
bookshelfScrollToTopRequest++
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
}
fun handleMainDestinationClick(index: Int, destination: MainDestination) {
if (destination == MainDestination.Bookshelf) {
val now = SystemClock.uptimeMillis()
if (
(pagerState.currentPage == index || pagerState.targetPage == index) &&
now - lastBookshelfNavClickTime <= ViewConfiguration.getDoubleTapTimeout()
) {
requestBookshelfScrollToTop(index)
lastBookshelfNavClickTime = 0L
return
}
lastBookshelfNavClickTime = now
if (
destination == MainDestination.Bookshelf &&
pagerState.currentPage == index &&
pagerState.targetPage == index &&
!isBookshelfAtTop
) {
requestBookshelfScrollToTop()
return
}
coroutineScope.launch {
pagerState.animateScrollToPage(index)
@@ -434,6 +426,9 @@ fun MainScreen(
MainDestination.Bookshelf -> BookshelfScreen(
scrollToTopRequest = bookshelfScrollToTopRequest,
onScrollStateChanged = { isAtTop ->
isBookshelfAtTop = isAtTop
},
onBookClick = { book ->
context.startActivityForBook(book)
},
@@ -529,13 +524,6 @@ fun MainScreen(
onClick = {
handleMainDestinationClick(index, destination)
},
onDoubleClick = if (destination == MainDestination.Bookshelf) {
{
requestBookshelfScrollToTop(index)
}
} else {
null
},
modifier = Modifier
.defaultMinSize(minWidth = 76.dp)
.semantics(mergeDescendants = true) {
@@ -44,6 +44,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
@@ -76,6 +77,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -156,6 +158,7 @@ import sh.calvin.reorderable.rememberReorderableLazyGridState
fun BookshelfScreen(
viewModel: BookshelfViewModel = koinViewModel(),
scrollToTopRequest: Long = 0L,
onScrollStateChanged: (isAtTop: Boolean) -> Unit = {},
onBookClick: (BookShelfItem) -> Unit,
onBookLongClick: (book: BookShelfItem, sharedCoverKey: String?) -> Unit,
onNavigateToSearch: (String) -> Unit,
@@ -233,6 +236,14 @@ fun BookshelfScreen(
initialPage = uiState.selectedGroupIndex.coerceAtLeast(0),
pageCount = { uiState.groups.size }
)
val folderGridState = rememberLazyGridState()
val standaloneSearchGridState = rememberLazyGridState()
val groupGridStates = mutableMapOf<Long, LazyGridState>()
uiState.groups.forEach { group ->
key(group.groupId) {
groupGridStates[group.groupId] = rememberLazyGridState()
}
}
val latestGroups by rememberUpdatedState(uiState.groups)
val latestSelectedGroupId by rememberUpdatedState(uiState.selectedGroupId)
@@ -281,6 +292,20 @@ fun BookshelfScreen(
uiState.isSearch && uiState.groups.none { it.groupId == currentGroupId }
}
}
val isShowingFolderRoot =
bookGroupStyle == 2 && isInFolderRoot && !isUsingStandaloneSearchGroup
LaunchedEffect(folderGridState, isShowingFolderRoot) {
if (isShowingFolderRoot) {
snapshotFlow { !folderGridState.canScrollBackward }
.distinctUntilChanged()
.collect(onScrollStateChanged)
}
}
LaunchedEffect(scrollToTopRequest, isShowingFolderRoot) {
if (scrollToTopRequest > 0L && isShowingFolderRoot) {
folderGridState.animateScrollToItem(0)
}
}
val currentGroupBookCount by remember { derivedStateOf { uiState.currentGroupBookCount } }
val clearSelection = {
@@ -726,6 +751,9 @@ fun BookshelfScreen(
}
) { isRoot ->
if (uiState.groups.isEmpty() && !uiState.isSearch) {
LaunchedEffect(Unit) {
onScrollStateChanged(true)
}
if (!uiState.isInitialLoading) {
EmptyMessage(
modifier = Modifier
@@ -743,6 +771,7 @@ fun BookshelfScreen(
val isGridMode = bookshelfFolderLayoutMode != 0
FastScrollLazyVerticalGrid(
columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)),
state = folderGridState,
modifier = Modifier
.fillMaxSize()
.then(
@@ -815,6 +844,8 @@ fun BookshelfScreen(
if (isUsingStandaloneSearchGroup) {
BookshelfPage(
scrollToTopRequest = scrollToTopRequest,
gridState = standaloneSearchGridState,
onScrollStateChanged = onScrollStateChanged,
paddingValues = paddingValues,
books = uiState.items,
uiState = uiState,
@@ -862,6 +893,8 @@ fun BookshelfScreen(
isSelectedGroup
BookshelfPage(
scrollToTopRequest = scrollToTopRequest,
gridState = groupGridStates.getValue(group.groupId),
onScrollStateChanged = onScrollStateChanged,
paddingValues = paddingValues,
books = books,
uiState = uiState,
@@ -1212,6 +1245,8 @@ private data class BookshelfEditStickySummary(
@Composable
fun BookshelfPage(
scrollToTopRequest: Long,
gridState: LazyGridState,
onScrollStateChanged: (isAtTop: Boolean) -> Unit,
paddingValues: PaddingValues,
books: ImmutableList<BookUiItem>,
uiState: BookshelfUiState,
@@ -1230,6 +1265,11 @@ fun BookshelfPage(
sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null,
) {
LaunchedEffect(books.isEmpty(), isCurrentPage) {
if (books.isEmpty() && isCurrentPage) {
onScrollStateChanged(true)
}
}
if (books.isEmpty()) {
if (!isCurrentPage) return
if (uiState.isSearch) {
@@ -1297,12 +1337,18 @@ fun BookshelfPage(
val gridInnerHorizontalPadding = totalHorizontalPadding / 2
val hapticFeedback = LocalHapticFeedback.current
val displayBooks = draggingBooks ?: pendingSavedBooks ?: books
val gridState = rememberLazyGridState()
LaunchedEffect(scrollToTopRequest, isCurrentPage) {
if (scrollToTopRequest > 0L && isCurrentPage) {
gridState.animateScrollToItem(0)
}
}
LaunchedEffect(gridState, isCurrentPage) {
if (isCurrentPage) {
snapshotFlow { !gridState.canScrollBackward }
.distinctUntilChanged()
.collect(onScrollStateChanged)
}
}
val reorderableState = rememberReorderableLazyGridState(gridState) { from, to ->
if (canReorderBooks) {
onMoveBook(from.index, to.index, displayBooks)
@@ -52,6 +52,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
@@ -65,6 +66,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -140,13 +142,13 @@ class BookshelfViewModel(
protected val _eventChannel = Channel<BaseRuleEvent>()
val events = _eventChannel.receiveAsFlow()
val groupsFlow: StateFlow<List<BookGroup>> = bookGroupRepository.flowShow()
val groupsFlow: SharedFlow<List<BookGroup>> = bookGroupRepository.flowShow()
.onEach {
if (it.isNotEmpty()) {
isInitialLoadingFlow.value = false
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
val allGroupsFlow: StateFlow<List<BookGroup>> = bookGroupRepository.flowAll()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
@@ -195,7 +197,7 @@ class BookshelfViewModel(
}.distinctUntilChanged().flowOn(Dispatchers.Default)
@OptIn(ExperimentalCoroutinesApi::class)
private val allGroupBooksImmutableFlow: StateFlow<ImmutableMap<Long, ImmutableList<BookUiItem>>> =
private val allGroupBooksImmutableFlow: Flow<ImmutableMap<Long, ImmutableList<BookUiItem>>> =
combine(groupsFlow, sortConfigFlow) { groups, sortConfig ->
groups to sortConfig
}.flatMapLatest { (groups, sortConfig) ->
@@ -220,7 +222,6 @@ class BookshelfViewModel(
}
}.distinctUntilChanged()
.flowOn(Dispatchers.Default)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentMapOf())
private val visibleBooksFlow: Flow<List<BookUiItem>> = combine(
booksFlow,
@@ -9,7 +9,6 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -78,7 +77,6 @@ val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } }
@Composable
fun RowScope.FloatingBottomBarItem(
onClick: () -> Unit,
onDoubleClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
@@ -86,23 +84,11 @@ fun RowScope.FloatingBottomBarItem(
Column(
modifier
.clip(ContinuousCapsule)
.then(
if (onDoubleClick != null) {
Modifier.combinedClickable(
interactionSource = null,
indication = null,
role = Role.Tab,
onClick = onClick,
onDoubleClick = onDoubleClick,
)
} else {
Modifier.clickable(
interactionSource = null,
indication = null,
role = Role.Tab,
onClick = onClick,
)
}
.clickable(
interactionSource = null,
indication = null,
role = Role.Tab,
onClick = onClick,
)
.fillMaxHeight()
.weight(1f)