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