HapeLee 6 месяцев назад
Родитель
Сommit
cc673eeac0

+ 30 - 8
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt

@@ -5,6 +5,9 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.Crossfade
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
 import androidx.compose.animation.animateContentSize
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
@@ -82,6 +85,7 @@ import io.legado.app.ui.widget.components.book.SearchBookListItem
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.main.bookCoverSharedElementKey
 import kotlinx.coroutines.delay
 import org.koin.androidx.compose.koinViewModel
 import org.koin.compose.koinInject
@@ -89,7 +93,7 @@ import org.koin.compose.koinInject
 @SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
 @OptIn(
     ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class,
-    ExperimentalMaterial3ExpressiveApi::class
+    ExperimentalMaterial3ExpressiveApi::class, ExperimentalSharedTransitionApi::class
 )
 @Composable
 fun ExploreShowScreen(
@@ -98,7 +102,9 @@ fun ExploreShowScreen(
     exploreUrl: String?,
     onBack: () -> Unit,
     onBookClick: (SearchBook) -> Unit,
-    viewModel: ExploreShowViewModel = koinViewModel()
+    viewModel: ExploreShowViewModel = koinViewModel(),
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
 ) {
 
     LaunchedEffect(sourceUrl, exploreUrl, viewModel) {
@@ -448,7 +454,9 @@ fun ExploreShowScreen(
                                 book = item.book,
                                 shelfState = item.shelfState,
                                 onClick = { onBookClick(item.book) },
-                                modifier = Modifier.animateItem()
+                                modifier = Modifier.animateItem(),
+                                sharedTransitionScope = sharedTransitionScope,
+                                animatedVisibilityScope = animatedVisibilityScope,
                             )
                         }
 
@@ -480,7 +488,9 @@ fun ExploreShowScreen(
                                 book = item.book,
                                 shelfState = item.shelfState,
                                 onClick = { onBookClick(item.book) },
-                                modifier = Modifier.animateItem()
+                                modifier = Modifier.animateItem(),
+                                sharedTransitionScope = sharedTransitionScope,
+                                animatedVisibilityScope = animatedVisibilityScope,
                             )
                         }
 
@@ -500,33 +510,45 @@ fun ExploreShowScreen(
     }
 }
 
+@OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun ExploreBookItem(
     book: SearchBook,
     shelfState: BookShelfState,
     onClick: () -> Unit,
-    modifier: Modifier = Modifier
+    modifier: Modifier = Modifier,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
 ) {
     SearchBookListItem(
         book = book,
         shelfState = shelfState,
         onClick = onClick,
-        modifier = modifier
+        modifier = modifier,
+        sharedTransitionScope = sharedTransitionScope,
+        animatedVisibilityScope = animatedVisibilityScope,
+        sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
     )
 }
 
+@OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun ExploreBookGridItem(
     book: SearchBook,
     onClick: () -> Unit,
     shelfState: BookShelfState,
-    modifier: Modifier = Modifier
+    modifier: Modifier = Modifier,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
 ) {
     SearchBookGridItem(
         book = book,
         shelfState = shelfState,
         onClick = onClick,
-        modifier = modifier
+        modifier = modifier,
+        sharedTransitionScope = sharedTransitionScope,
+        animatedVisibilityScope = animatedVisibilityScope,
+        sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
     )
 }
 

+ 22 - 20
app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt

@@ -3,6 +3,8 @@ package io.legado.app.ui.book.info
 import androidx.compose.animation.AnimatedVisibilityScope
 import androidx.compose.animation.ExperimentalSharedTransitionApi
 import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.combinedClickable
@@ -12,6 +14,7 @@ import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
@@ -54,6 +57,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.blur
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
@@ -501,6 +505,12 @@ private fun BookInfoBackdrop(book: Book) {
         }
     }
 
+    val backdropAlpha by animateFloatAsState(
+        targetValue = if (showBackdropImage) 1f else 0f,
+        animationSpec = tween(800),
+        label = "BackdropFade"
+    )
+
     val backdropRequest = remember(cover, sourceOrigin, loadOnlyWifi, context) {
         buildCoverImageRequest(
             context = context,
@@ -516,15 +526,16 @@ private fun BookInfoBackdrop(book: Book) {
         0.42f
     )
     Box(modifier = Modifier.fillMaxSize()) {
-        if (!cover.isNullOrBlank() && showBackdropImage) {
+        if (!cover.isNullOrBlank()) {
             AsyncImage(
                 model = backdropRequest,
                 imageLoader = imageLoader,
                 contentDescription = null,
                 modifier = Modifier
                     .fillMaxWidth()
-                    .height(360.dp)
-                    .blur(24.dp),
+                    .height(480.dp)
+                    .blur(24.dp)
+                    .alpha(backdropAlpha),
                 contentScale = ContentScale.Crop,
             )
         }
@@ -540,10 +551,10 @@ private fun BookInfoBackdrop(book: Book) {
                     Brush.verticalGradient(
                         colorStops = arrayOf(
                             0f to Color.Transparent,
-                            0.18f to seedOverlay.copy(alpha = 0.10f),
-                            0.34f to seedOverlay.copy(alpha = 0.18f),
-                            0.52f to LegadoTheme.colorScheme.surface.copy(alpha = 0.82f),
-                            0.72f to LegadoTheme.colorScheme.surface,
+                            0.20f to seedOverlay.copy(alpha = 0.10f),
+                            0.40f to seedOverlay.copy(alpha = 0.18f),
+                            0.60f to LegadoTheme.colorScheme.surface.copy(alpha = 0.85f),
+                            0.80f to LegadoTheme.colorScheme.surface,
                             1f to LegadoTheme.colorScheme.surface,
                         )
                     )
@@ -687,25 +698,16 @@ private fun BookInfoHeader(
                         .width(112.dp)
                         .combinedClickable(onClick = onCoverClick, onLongClick = onCoverLongClick)
                 ) {
-                    val coverModifier = with(sharedTransitionScope) {
-                        if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) {
-                            Modifier
-                                .width(112.dp)
-                                .sharedElement(
-                                    sharedContentState = rememberSharedContentState(sharedCoverKey),
-                                    animatedVisibilityScope = animatedVisibilityScope,
-                                )
-                        } else {
-                            Modifier.width(112.dp)
-                        }
-                    }
                     CoilBookCover(
                         name = book.name,
                         author = book.author,
                         path = book.getDisplayCover(),
                         sourceOrigin = book.origin,
-                        modifier = coverModifier,
+                        modifier = Modifier.width(112.dp).aspectRatio(5f / 7f),
                         showLoadingPlaceholder = sharedCoverKey == null,
+                        sharedTransitionScope = sharedTransitionScope,
+                        animatedVisibilityScope = animatedVisibilityScope,
+                        sharedCoverKey = sharedCoverKey,
                     )
                 }
                 Column(

+ 11 - 2
app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt

@@ -1,6 +1,9 @@
 package io.legado.app.ui.book.search
 
 import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -91,18 +94,21 @@ import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
 import io.legado.app.ui.widget.components.tabRow.AppTabRow
 import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.ui.main.bookCoverSharedElementKey
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.distinctUntilChanged
 
-@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
+@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
 @Composable
 fun SearchScreen(
     viewModel: SearchViewModel,
     onBack: () -> Unit,
     onOpenBookInfo: (name: String, author: String, bookUrl: String) -> Unit,
     onOpenSourceManage: () -> Unit,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
 ) {
     val context = LocalContext.current
     val state by viewModel.uiState.collectAsStateWithLifecycle()
@@ -362,7 +368,10 @@ fun SearchScreen(
                                         shelfState = item.shelfState,
                                         onClick = {
                                             viewModel.onIntent(SearchIntent.OpenSearchBook(item.book))
-                                        }
+                                        },
+                                        sharedTransitionScope = sharedTransitionScope,
+                                        animatedVisibilityScope = animatedVisibilityScope,
+                                        sharedCoverKey = bookCoverSharedElementKey(item.book.bookUrl)
                                     )
                                 }
 

+ 30 - 4
app/src/main/java/io/legado/app/ui/main/MainActivity.kt

@@ -609,7 +609,18 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
                     )
                 }
 
-                entry<MainRouteSearch> { route ->
+                entry<MainRouteSearch>(
+                    metadata = NavDisplay.transitionSpec {
+                        fadeIn(animationSpec = tween(300)) togetherWith
+                                fadeOut(animationSpec = tween(300))
+                    } + NavDisplay.popTransitionSpec {
+                        fadeIn(animationSpec = tween(300)) togetherWith
+                                fadeOut(animationSpec = tween(300))
+                    } + NavDisplay.predictivePopTransitionSpec { _ ->
+                        fadeIn(animationSpec = tween(300)) togetherWith
+                                fadeOut(animationSpec = tween(300))
+                    }
+                ) { route ->
                     val searchViewModel = koinViewModel<SearchViewModel>()
                     val lifecycleOwner = LocalLifecycleOwner.current
 
@@ -657,7 +668,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
                         },
                         onOpenSourceManage = {
                             this@MainActivity.startActivity<BookSourceActivity>()
-                        }
+                        },
+                        sharedTransitionScope = this@SharedTransitionLayout,
+                        animatedVisibilityScope = LocalNavAnimatedContentScope.current,
                     )
                 }
 
@@ -730,7 +743,18 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
                     )
                 }
 
-                entry<MainRouteExploreShow> { route ->
+                entry<MainRouteExploreShow>(
+                    metadata = NavDisplay.transitionSpec {
+                        fadeIn(animationSpec = tween(300)) togetherWith
+                                fadeOut(animationSpec = tween(300))
+                    } + NavDisplay.popTransitionSpec {
+                        fadeIn(animationSpec = tween(300)) togetherWith
+                                fadeOut(animationSpec = tween(300))
+                    } + NavDisplay.predictivePopTransitionSpec { _ ->
+                        fadeIn(animationSpec = tween(300)) togetherWith
+                                fadeOut(animationSpec = tween(300))
+                    }
+                ) { route ->
                     ExploreShowScreen(
                         title = route.title ?: "探索",
                         sourceUrl = route.sourceUrl,
@@ -745,7 +769,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
                                     bookUrl = book.bookUrl
                                 )
                             )
-                        }
+                        },
+                        sharedTransitionScope = this@SharedTransitionLayout,
+                        animatedVisibilityScope = LocalNavAnimatedContentScope.current,
                     )
                 }
                 }

+ 7 - 1
app/src/main/java/io/legado/app/ui/main/MainScreen.kt

@@ -349,7 +349,13 @@ fun MainScreen(
                 ) {
                     HorizontalPager(
                         state = pagerState,
-                        modifier = Modifier.fillMaxSize(),
+                        modifier = Modifier
+                            .fillMaxSize()
+                            .then(
+                                with(sharedTransitionScope) {
+                                    if (this != null) Modifier.skipToLookaheadSize() else Modifier
+                                }
+                            ),
                         userScrollEnabled = true,
                         beyondViewportPageCount = 1
                     ) { page ->

+ 5 - 12
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -536,28 +536,21 @@ fun BookItem(
             }
         } else null,
         cover = { modifier ->
-            val coverModifier = with(sharedTransitionScope) {
-                if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) {
-                    Modifier.fillMaxWidth().sharedElement(
-                        sharedContentState = rememberSharedContentState(sharedCoverKey),
-                        animatedVisibilityScope = animatedVisibilityScope,
-                    )
-                } else {
-                    Modifier.fillMaxWidth()
-                }
-            }
             BookshelfCover(
                 name = book.name,
                 author = book.author,
                 path = book.getDisplayCover(),
                 isUpdating = isUpdating,
                 modifier = modifier,
-                coverModifier = coverModifier,
+                coverModifier = Modifier.fillMaxWidth().aspectRatio(5f / 7f),
                 sourceOrigin = book.origin,
                 badgeText = if (layoutMode != 0) unreadText else null,
                 showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew,
                 leftBottomText = matchedSourceLabel ?: bookTypeLabel,
-                showLoadingPlaceholder = sharedCoverKey == null,
+                showLoadingPlaceholder = true,
+                sharedTransitionScope = sharedTransitionScope,
+                animatedVisibilityScope = animatedVisibilityScope,
+                sharedCoverKey = sharedCoverKey,
             )
         },
         title = book.name,

+ 98 - 71
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -632,7 +632,12 @@ fun BookshelfScreen(
                     FastScrollLazyVerticalGrid(
                         columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)),
                         modifier = Modifier
-                            .fillMaxSize(),
+                            .fillMaxSize()
+                            .then(
+                                with(sharedTransitionScope) {
+                                    if (this != null) Modifier.skipToLookaheadSize() else Modifier
+                                }
+                            ),
                         contentPadding = adaptiveContentPaddingBookshelf(
                             top = paddingValues.calculateTopPadding(),
                             bottom = 120.dp,
@@ -717,7 +722,13 @@ fun BookshelfScreen(
                     } else {
                         HorizontalPager(
                             state = pagerState,
-                            modifier = Modifier.fillMaxSize(),
+                            modifier = Modifier
+                                .fillMaxSize()
+                                .then(
+                                    with(sharedTransitionScope) {
+                                        if (this != null) Modifier.skipToLookaheadSize() else Modifier
+                                    }
+                                ),
                             beyondViewportPageCount = 2,
                             key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
                         ) { pageIndex ->
@@ -1078,82 +1089,98 @@ fun BookshelfPage(
             onDragFinished()
         }
     }
-    FastScrollLazyVerticalGrid(
-        columns = GridCells.Fixed(columns.coerceAtLeast(1)),
-        state = gridState,
+
+    Box(
         modifier = Modifier
-            .fillMaxSize(),
-        contentPadding = adaptiveContentPaddingBookshelf(
-            top = paddingValues.calculateTopPadding(),
-            bottom = 120.dp,
-            horizontal = if (isGridMode) 8.dp else 4.dp
-        ),
-        verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
-        horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
-        showFastScroll = BookshelfConfig.showBookshelfFastScroller
+            .fillMaxSize()
+            .then(
+                with(sharedTransitionScope) {
+                    if (this != null) Modifier.skipToLookaheadSize() else Modifier
+                }
+            )
     ) {
-        items(displayBooks, key = { it.bookUrl }) { book ->
-            val isSelected = selectedBookUrls.contains(book.bookUrl)
-            ReorderableItem(
-                state = reorderableState,
-                key = book.bookUrl,
-                enabled = canReorderBooks
-            ) {
-                BookItem(
-                    book = book,
-                    modifier = Modifier.then(
-                        if (canReorderBooks) {
-                            Modifier.longPressDraggableHandle(
-                                onDragStarted = {
-                                    onDragStarted(displayBooks)
-                                    hapticFeedback.performHapticFeedback(
-                                        HapticFeedbackType.GestureThresholdActivate
-                                    )
-                                },
-                                onDragStopped = {
-                                    hapticFeedback.performHapticFeedback(
-                                        HapticFeedbackType.GestureEnd
-                                    )
-                                }
-                            )
-                        } else {
-                            Modifier
-                        }
-                    ),
-                    layoutMode = bookshelfLayoutMode,
-                    isSelected = isSelected,
-                    gridStyle = BookshelfConfig.bookshelfGridLayout,
-                    isCompact = BookshelfConfig.bookshelfLayoutCompact,
-                    isUpdating = uiState.updatingBooks.contains(book.bookUrl),
-                    titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
-                    titleCenter = BookshelfConfig.bookshelfTitleCenter,
-                    titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
-                    coverShadow = BookshelfConfig.bookshelfCoverShadow,
-                    isSearchMode = uiState.isSearch,
-                    searchKey = uiState.searchKey,
-                    sharedTransitionScope = sharedTransitionScope,
-                    animatedVisibilityScope = animatedVisibilityScope,
-                    sharedCoverKey = bookCoverSharedElementKey(book.bookUrl),
-                    onClick = {
-                        if (uiState.isEditMode) {
-                            onToggleBookSelection(book)
-                        } else {
-                            onBookClick(book)
-                        }
-                    },
-                    onLongClick = if (canReorderBooks) {
-                        null
-                    } else {
-                        {
+        FastScrollLazyVerticalGrid(
+            columns = GridCells.Fixed(columns.coerceAtLeast(1)),
+            state = gridState,
+            modifier = Modifier
+                .fillMaxSize()
+                .then(
+                    with(sharedTransitionScope) {
+                        if (this != null) Modifier.skipToLookaheadSize() else Modifier
+                    }
+                ),
+            contentPadding = adaptiveContentPaddingBookshelf(
+                top = paddingValues.calculateTopPadding(),
+                bottom = 120.dp,
+                horizontal = if (isGridMode) 8.dp else 4.dp
+            ),
+            verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
+            horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
+            showFastScroll = BookshelfConfig.showBookshelfFastScroller
+        ) {
+            items(displayBooks, key = { it.bookUrl }) { book ->
+                val isSelected = selectedBookUrls.contains(book.bookUrl)
+                ReorderableItem(
+                    state = reorderableState,
+                    key = book.bookUrl,
+                    enabled = canReorderBooks
+                ) {
+                    BookItem(
+                        book = book,
+                        modifier = Modifier.then(
+                            if (canReorderBooks) {
+                                Modifier.longPressDraggableHandle(
+                                    onDragStarted = {
+                                        onDragStarted(displayBooks)
+                                        hapticFeedback.performHapticFeedback(
+                                            HapticFeedbackType.GestureThresholdActivate
+                                        )
+                                    },
+                                    onDragStopped = {
+                                        hapticFeedback.performHapticFeedback(
+                                            HapticFeedbackType.GestureEnd
+                                        )
+                                    }
+                                )
+                            } else {
+                                Modifier
+                            }
+                        ),
+                        layoutMode = bookshelfLayoutMode,
+                        isSelected = isSelected,
+                        gridStyle = BookshelfConfig.bookshelfGridLayout,
+                        isCompact = BookshelfConfig.bookshelfLayoutCompact,
+                        isUpdating = uiState.updatingBooks.contains(book.bookUrl),
+                        titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
+                        titleCenter = BookshelfConfig.bookshelfTitleCenter,
+                        titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
+                        coverShadow = BookshelfConfig.bookshelfCoverShadow,
+                        isSearchMode = uiState.isSearch,
+                        searchKey = uiState.searchKey,
+                        sharedTransitionScope = sharedTransitionScope,
+                        animatedVisibilityScope = animatedVisibilityScope,
+                        sharedCoverKey = if (isCurrentPage) bookCoverSharedElementKey(book.bookUrl) else null,
+                        onClick = {
                             if (uiState.isEditMode) {
                                 onToggleBookSelection(book)
                             } else {
-                                onBookLongClick(book)
+                                onBookClick(book)
+                            }
+                        },
+                        onLongClick = if (canReorderBooks) {
+                            null
+                        } else {
+                            {
+                                if (uiState.isEditMode) {
+                                    onToggleBookSelection(book)
+                                } else {
+                                    onBookLongClick(book)
+                                }
                             }
                         }
-                    }
-                )
+                    )
+                }
             }
         }
     }
-}
+}

+ 30 - 53
app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt

@@ -1,5 +1,8 @@
 package io.legado.app.ui.widget.components.book
 
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Column
@@ -18,8 +21,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.Shuffle
-import androidx.compose.material.icons.filled.SwapHoriz
-import androidx.compose.material.icons.filled.Warning
 import androidx.compose.material3.Surface
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
@@ -34,7 +35,7 @@ import androidx.compose.ui.unit.sp
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.domain.model.BookShelfState
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.cover.Cover
+import io.legado.app.ui.widget.components.cover.CoilBookCover
 import io.legado.app.ui.widget.components.text.AppText
 import kotlinx.coroutines.flow.Flow
 
@@ -54,47 +55,33 @@ fun SearchBookListItem(
     )
 }
 
+@OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun SearchBookListItem(
     book: SearchBook,
     shelfState: BookShelfState,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
+    sharedCoverKey: String? = null,
 ) {
-    val badgeContent: (@Composable RowScope.() -> Unit)? = when (shelfState) {
-        BookShelfState.IN_SHELF -> {
-            {
-                androidx.compose.material3.Icon(
-                    imageVector = Icons.Default.Check,
-                    contentDescription = "已在书架",
-                    modifier = Modifier.size(12.dp),
-                )
-            }
-        }
-
-        BookShelfState.SAME_NAME_AUTHOR -> {
-            {
-                androidx.compose.material3.Icon(
-                    imageVector = Icons.Default.Shuffle,
-                    contentDescription = "同名书籍",
-                    modifier = Modifier.size(12.dp),
-                )
-            }
-        }
-
-        BookShelfState.NOT_IN_SHELF -> null
-    }
-
     Row(
         modifier = modifier
             .fillMaxWidth()
             .clickable(onClick = onClick)
             .padding(horizontal = 16.dp, vertical = 8.dp)
     ) {
-        Cover(
+        CoilBookCover(
+            name = book.name,
+            author = book.author,
             path = book.coverUrl,
-            modifier = Modifier.width(72.dp),
-            badgeContent = badgeContent,
+            modifier = Modifier.width(72.dp).aspectRatio(5f / 7f),
+            sourceOrigin = book.origin,
+            sharedTransitionScope = sharedTransitionScope,
+            animatedVisibilityScope = animatedVisibilityScope,
+            sharedCoverKey = sharedCoverKey,
+            showLoadingPlaceholder = sharedCoverKey == null
         )
 
         Spacer(modifier = Modifier.width(8.dp))
@@ -180,45 +167,35 @@ fun SearchBookGridItem(
     )
 }
 
+@OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun SearchBookGridItem(
     book: SearchBook,
     shelfState: BookShelfState,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
+    sharedCoverKey: String? = null,
 ) {
-    val badgeText: String? = when (shelfState) {
-        BookShelfState.IN_SHELF -> "已在书架"
-        BookShelfState.SAME_NAME_AUTHOR -> "同名书籍"
-        BookShelfState.NOT_IN_SHELF -> null
-    }
-
-    val badgeContent: (@Composable RowScope.() -> Unit)? = if (!badgeText.isNullOrBlank()) {
-        {
-            AppText(
-                text = badgeText,
-                style = LegadoTheme.typography.labelSmall.copy(
-                    fontWeight = FontWeight.Bold,
-                    fontSize = 9.sp,
-                )
-            )
-        }
-    } else {
-        null
-    }
-
     Column(
         modifier = modifier
             .width(IntrinsicSize.Min)
             .clickable(onClick = onClick)
             .padding(4.dp)
     ) {
-        Cover(
+        CoilBookCover(
+            name = book.name,
+            author = book.author,
             path = book.coverUrl,
             modifier = Modifier
                 .fillMaxWidth()
-                .aspectRatio(12 / 17f),
-            badgeContent = badgeContent,
+                .aspectRatio(5f / 7f),
+            sourceOrigin = book.origin,
+            sharedTransitionScope = sharedTransitionScope,
+            animatedVisibilityScope = animatedVisibilityScope,
+            sharedCoverKey = sharedCoverKey,
+            showLoadingPlaceholder = sharedCoverKey == null
         )
 
         Spacer(modifier = Modifier.height(4.dp))

+ 10 - 0
app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt

@@ -1,5 +1,8 @@
 package io.legado.app.ui.widget.components.cover
 
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
@@ -14,6 +17,7 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppLinearProgressIndicator
 import io.legado.app.ui.widget.components.card.TextCard
 
+@OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun BookshelfCover(
     name: String?,
@@ -28,6 +32,9 @@ fun BookshelfCover(
     sourceOrigin: String? = null,
     onLoadFinish: (() -> Unit)? = null,
     showLoadingPlaceholder: Boolean = true,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
+    sharedCoverKey: String? = null,
 ) {
     Box(modifier = modifier) {
         CoilBookCover(
@@ -38,6 +45,9 @@ fun BookshelfCover(
             sourceOrigin = sourceOrigin,
             onLoadFinish = onLoadFinish,
             showLoadingPlaceholder = showLoadingPlaceholder,
+            sharedTransitionScope = sharedTransitionScope,
+            animatedVisibilityScope = animatedVisibilityScope,
+            sharedCoverKey = sharedCoverKey,
         )
 
         if (!badgeText.isNullOrEmpty()) {

+ 55 - 91
app/src/main/java/io/legado/app/ui/widget/components/cover/CoilBookCover.kt

@@ -6,11 +6,13 @@ import android.text.Layout
 import android.text.StaticLayout
 import android.text.TextPaint
 import android.text.TextUtils
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.background
 import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
@@ -27,6 +29,7 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
@@ -34,17 +37,15 @@ import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
 import androidx.core.graphics.withSave
 import coil.compose.AsyncImage
 import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.card.NormalCard
-import kotlinx.coroutines.delay
 import org.koin.compose.koinInject
 import io.legado.app.model.BookCover as BookCoverModel
 
+@OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun CoilBookCover(
     name: String?,
@@ -55,6 +56,9 @@ fun CoilBookCover(
     onLoadFinish: (() -> Unit)? = null,
     ignoreUseDefaultCover: Boolean = false,
     showLoadingPlaceholder: Boolean = true,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
+    sharedCoverKey: String? = null,
 ) {
     val context = LocalContext.current
     val isNight = isSystemInDarkTheme()
@@ -62,11 +66,7 @@ fun CoilBookCover(
     val useDefault = !ignoreUseDefaultCover && CoverConfig.useDefaultCover
     val finalPath = if (useDefault) null else path
 
-    val randomPath = remember(
-        name, author, path, isNight,
-        CoverConfig.defaultCover,
-        CoverConfig.defaultCoverDark
-    ) {
+    val randomPath = remember(name, author, path, isNight) {
         BookCoverModel.getRandomDefaultPath(
             seed = name ?: author ?: path ?: "",
             isNight = isNight
@@ -74,57 +74,46 @@ fun CoilBookCover(
     }
 
     val hasCustomDefault = !randomPath.isNullOrBlank()
-
     var isOnlineCoverLoaded by remember(path) { mutableStateOf(false) }
-    var isFinalPathLoaded by remember(path) { mutableStateOf(false) }
-    var isFinalPathLoadFinished by remember(path) { mutableStateOf(false) }
-    var isPlaceholderAllowed by remember(path, showLoadingPlaceholder) {
-        mutableStateOf(showLoadingPlaceholder)
-    }
-    LaunchedEffect(finalPath) {
-        isOnlineCoverLoaded = false
-        isFinalPathLoaded = false
-        isFinalPathLoadFinished = finalPath == null
-    }
-    LaunchedEffect(finalPath, showLoadingPlaceholder) {
-        if (showLoadingPlaceholder) {
-            isPlaceholderAllowed = true
-        } else {
-            isPlaceholderAllowed = false
-            delay(600)
-            isPlaceholderAllowed = true
-        }
-    }
 
-    NormalCard(
-        cornerRadius = 4.dp,
-        containerColor = if (!hasCustomDefault && !isOnlineCoverLoaded) {
-            LegadoTheme.colorScheme.surfaceContainerLow
-        } else
-            LegadoTheme.colorScheme.surfaceContainerLow.copy(alpha = 0f)
-    ){
-        BoxWithConstraints(
-            modifier = modifier
-                .aspectRatio(5f / 7f)
-                .then(
-                    if (CoverConfig.coverShowShadow) {
-                        Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
+    Box(
+        modifier = modifier
+            .then(
+                if (CoverConfig.coverShowShadow) {
+                    Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
+                } else Modifier
+            )
+            .background(
+                if (!hasCustomDefault && !isOnlineCoverLoaded) {
+                    LegadoTheme.colorScheme.surfaceContainerLow
+                } else Color.Transparent,
+                RoundedCornerShape(4.dp)
+            )
+            .clip(RoundedCornerShape(4.dp))
+    ) {
+        val imageContentModifier = Modifier
+            .fillMaxSize()
+            .then(
+                with(sharedTransitionScope) {
+                    if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) {
+                        Modifier.sharedElement(
+                            sharedContentState = rememberSharedContentState(sharedCoverKey),
+                            animatedVisibilityScope = animatedVisibilityScope,
+                            renderInOverlayDuringTransition = true
+                        )
                     } else Modifier
-                )
-        ) {
-            val density = LocalDensity.current
-            val iconSizeDp = with(density) {
-                (minOf(maxWidth, maxHeight).toPx() / 3f).toDp()
-            }
+                }
+            )
 
-            if (hasCustomDefault && !isFinalPathLoaded) {
+        Box(modifier = imageContentModifier) {
+            if (hasCustomDefault && !isOnlineCoverLoaded) {
                 AsyncImage(
                     model = buildCoverImageRequest(
                         context = context,
                         data = randomPath,
                         sourceOrigin = null,
                         loadOnlyWifi = false,
-                        crossfade = true,
+                        crossfade = showLoadingPlaceholder,
                     ),
                     contentDescription = null,
                     imageLoader = koinInject(),
@@ -140,7 +129,7 @@ fun CoilBookCover(
                         data = finalPath,
                         sourceOrigin = sourceOrigin,
                         loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
-                        crossfade = true,
+                        crossfade = showLoadingPlaceholder,
                     ),
                     contentDescription = null,
                     imageLoader = koinInject(),
@@ -148,13 +137,10 @@ fun CoilBookCover(
                     modifier = Modifier.fillMaxSize(),
                     onSuccess = {
                         isOnlineCoverLoaded = true
-                        isFinalPathLoaded = true
-                        isFinalPathLoadFinished = true
                         onLoadFinish?.invoke()
                     },
                     onError = {
                         isOnlineCoverLoaded = false
-                        isFinalPathLoadFinished = true
                         onLoadFinish?.invoke()
                     }
                 )
@@ -163,28 +149,24 @@ fun CoilBookCover(
                     onLoadFinish?.invoke()
                 }
             }
+        }
 
-            val showPlaceholder = isPlaceholderAllowed && (showLoadingPlaceholder || isFinalPathLoadFinished)
-
-            if (!hasCustomDefault && !isOnlineCoverLoaded && showPlaceholder) {
+        if (showLoadingPlaceholder && !isOnlineCoverLoaded) {
+            if (!hasCustomDefault) {
                 Icon(
                     Icons.Default.Book,
                     contentDescription = null,
                     tint = LegadoTheme.colorScheme.secondary,
                     modifier = Modifier
-                        .size(iconSizeDp)
+                        .fillMaxSize(0.35f)
                         .align(Alignment.Center)
                 )
             }
-
-            if (!isOnlineCoverLoaded && showPlaceholder) {
-                CoverTextOverlay(
-                    name = name,
-                    author = author,
-                    isNight = isNight
-                )
-            }
-
+            CoverTextOverlay(
+                name = name,
+                author = author,
+                isNight = isNight
+            )
         }
     }
 }
@@ -196,8 +178,7 @@ private fun CoverTextOverlay(
     isNight: Boolean
 ) {
     val showName = if (isNight) CoverConfig.coverShowNameN else CoverConfig.coverShowName
-    val showAuthor =
-        (if (isNight) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor) && showName
+    val showAuthor = (if (isNight) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor) && showName
 
     if (!showName && !showAuthor) return
 
@@ -231,9 +212,7 @@ private fun CoverTextOverlay(
 
                 if (isHorizontal) {
                     val maxWidth = (viewWidth * 0.8f).toInt()
-                    val textPaint = TextPaint(paint).apply {
-                        textAlign = Paint.Align.LEFT
-                    }
+                    val textPaint = TextPaint(paint).apply { textAlign = Paint.Align.LEFT }
                     val layout = StaticLayout.Builder
                         .obtain(name, 0, name.length, textPaint, maxWidth)
                         .setAlignment(Layout.Alignment.ALIGN_CENTER)
@@ -296,12 +275,7 @@ private fun CoverTextOverlay(
                     }
                 }
                 if (isHorizontal) {
-                    val authorText = TextUtils.ellipsize(
-                        author,
-                        TextPaint(paint),
-                        viewWidth * 0.9f,
-                        TextUtils.TruncateAt.END
-                    )
+                    val authorText = TextUtils.ellipsize(author, TextPaint(paint), viewWidth * 0.9f, TextUtils.TruncateAt.END)
                     if (CoverConfig.coverShowStroke) {
                         val strokePaint = Paint(paint).apply {
                             color = Color.White.toArgb()
@@ -309,19 +283,9 @@ private fun CoverTextOverlay(
                             strokeWidth = paint.textSize / 10
                             clearShadowLayer()
                         }
-                        nativeCanvas.drawText(
-                            authorText.toString(),
-                            viewWidth / 2,
-                            viewHeight * 0.75f,
-                            strokePaint
-                        )
+                        nativeCanvas.drawText(authorText.toString(), viewWidth / 2, viewHeight * 0.75f, strokePaint)
                     }
-                    nativeCanvas.drawText(
-                        authorText.toString(),
-                        viewWidth / 2,
-                        viewHeight * 0.75f,
-                        paint
-                    )
+                    nativeCanvas.drawText(authorText.toString(), viewWidth / 2, viewHeight * 0.75f, paint)
                 } else {
                     val startX = viewWidth * 0.84f
                     val fm = paint.fontMetrics