Просмотр исходного кода

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

HapeLee 4 месяцев назад
Родитель
Сommit
45c403fe79

+ 13 - 25
app/src/main/java/io/legado/app/ui/main/MainScreen.kt

@@ -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) }
-    fun requestBookshelfScrollToTop(index: Int) {
+    var isBookshelfAtTop by remember { mutableStateOf(true) }
+    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) {
-            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 {
         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) {

+ 47 - 1
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -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)

+ 5 - 4
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -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,

+ 5 - 19
app/src/main/java/io/legado/app/ui/widget/components/FloatingBottomBar.kt

@@ -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(
-                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()
             .fillMaxHeight()
             .weight(1f)
             .weight(1f)