diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt index cda365770..06557ea0e 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt +++ b/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) ) } diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt index c734a27e3..cd1eeb7c1 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt +++ b/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( diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt index c8380994f..52fcd2b79 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt +++ b/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) ) } diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index cf44fdc17..add2cc6f9 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -609,7 +609,18 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { ) } - entry { route -> + entry( + 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() val lifecycleOwner = LocalLifecycleOwner.current @@ -657,7 +668,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { }, onOpenSourceManage = { this@MainActivity.startActivity() - } + }, + sharedTransitionScope = this@SharedTransitionLayout, + animatedVisibilityScope = LocalNavAnimatedContentScope.current, ) } @@ -730,7 +743,18 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { ) } - entry { route -> + entry( + 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, ) } } diff --git a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt index c2af0ae83..552ca5d13 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt +++ b/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 -> diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt index 8ff17b66f..2f8ee637b 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt +++ b/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, diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt index 6dfb4dd86..eb3760f77 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt +++ b/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) + } } } - } - ) + ) + } } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt index 01d96e5e5..c9804b9aa 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt +++ b/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)) diff --git a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt index 0dff44ab2..46db58053 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt +++ b/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()) { diff --git a/app/src/main/java/io/legado/app/ui/widget/components/cover/CoilBookCover.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/CoilBookCover.kt index dd8b1899a..0e723690b 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/cover/CoilBookCover.kt +++ b/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