diff --git a/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt b/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt index 1b42add4d..85d469fa0 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt @@ -7,8 +7,10 @@ import android.text.StaticLayout import android.text.TextPaint import android.text.TextUtils import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterExitState import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.animateFloat import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme @@ -25,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -47,6 +50,9 @@ import io.legado.app.ui.theme.LegadoTheme import org.koin.compose.koinInject import io.legado.app.model.BookCover as BookCoverModel +private const val SharedCoverRadiusCacheMaxSize = 256 +private val sharedCoverRadiusCache = mutableStateMapOf() + @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CoilBookCover( @@ -81,107 +87,152 @@ fun CoilBookCover( mutableStateOf(sharedCoverKey != null && finalPath != null) } - Box( - modifier = modifier - .aspectRatio(5f / 7f) - .then( - with(sharedTransitionScope) { - if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) { - Modifier.sharedElement( - sharedContentState = rememberSharedContentState(sharedCoverKey), - animatedVisibilityScope = animatedVisibilityScope, - renderInOverlayDuringTransition = true - ) - } else Modifier - } - ) - .then( - if (CoverConfig.coverShowShadow) { - Modifier.shadow(4.dp, RoundedCornerShape(radius)) - } else Modifier - ) - .background( - if (!hasCustomDefault && !isOnlineCoverLoaded) { - LegadoTheme.colorScheme.surfaceContainerLow - } else Color.Transparent, - RoundedCornerShape(radius) - ) - .clip(RoundedCornerShape(radius)) - ) { - if (hasCustomDefault && !isOnlineCoverLoaded) { - key(randomPath) { - AsyncImage( - model = buildCoverImageRequest( - context = context, - data = randomPath, - sourceOrigin = null, - loadOnlyWifi = false, - crossfade = showLoadingPlaceholder, - memoryCacheKey = randomPath, - ), - contentDescription = null, - imageLoader = koinInject(), - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .clip(RoundedCornerShape(radius)) - ) - } - } + val transitionRadius = rememberSharedCoverTransitionRadius( + sharedCoverKey = sharedCoverKey, + radius = radius, + animatedVisibilityScope = animatedVisibilityScope + ) + val shape = remember(transitionRadius) { RoundedCornerShape(transitionRadius) } - if (finalPath != null) { - key(finalPath) { - AsyncImage( - model = buildCoverImageRequest( - context = context, - data = finalPath, - sourceOrigin = sourceOrigin, - loadOnlyWifi = CoverConfig.loadCoverOnlyWifi, - crossfade = showLoadingPlaceholder, - memoryCacheKey = finalPath, - ), - contentDescription = null, - imageLoader = koinInject(), - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .clip(RoundedCornerShape(4.dp)), - onSuccess = { - isOnlineCoverLoaded = true - onLoadFinish?.invoke() - }, - onError = { - isOnlineCoverLoaded = false - onLoadFinish?.invoke() + key(path, sharedCoverKey) { + Box( + modifier = modifier + .aspectRatio(5f / 7f) + .then( + with(sharedTransitionScope) { + if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) { + Modifier.sharedElement( + sharedContentState = rememberSharedContentState(sharedCoverKey), + animatedVisibilityScope = animatedVisibilityScope, + clipInOverlayDuringTransition = OverlayClip(shape) + ) + } else Modifier } ) - } - } else { - LaunchedEffect(Unit) { - onLoadFinish?.invoke() - } - } + .then( + if (CoverConfig.coverShowShadow) { + Modifier.shadow(4.dp, shape) + } else Modifier + ) + .background( + if (!hasCustomDefault && !isOnlineCoverLoaded) { + LegadoTheme.colorScheme.surfaceContainerLow + } else Color.Transparent, + shape + ) + .clip(shape) + ) { + Box(modifier = Modifier.fillMaxSize()) { + if (hasCustomDefault && !isOnlineCoverLoaded) { + AsyncImage( + model = buildCoverImageRequest( + context = context, + data = randomPath, + sourceOrigin = null, + loadOnlyWifi = false, + crossfade = showLoadingPlaceholder, + memoryCacheKey = randomPath, + ), + contentDescription = null, + imageLoader = koinInject(), + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + ) + } - if (showLoadingPlaceholder && !isOnlineCoverLoaded) { - if (!hasCustomDefault) { - Icon( - Icons.Default.Book, - contentDescription = null, - tint = LegadoTheme.colorScheme.secondary, - modifier = Modifier - .fillMaxSize(0.35f) - .align(Alignment.Center) + if (finalPath != null) { + AsyncImage( + model = buildCoverImageRequest( + context = context, + data = finalPath, + sourceOrigin = sourceOrigin, + loadOnlyWifi = CoverConfig.loadCoverOnlyWifi, + crossfade = showLoadingPlaceholder, + memoryCacheKey = finalPath, + ), + contentDescription = null, + imageLoader = koinInject(), + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + onSuccess = { + isOnlineCoverLoaded = true + onLoadFinish?.invoke() + }, + onError = { + isOnlineCoverLoaded = false + onLoadFinish?.invoke() + } + ) + } else { + LaunchedEffect(Unit) { + onLoadFinish?.invoke() + } + } + } + + if (showLoadingPlaceholder && !isOnlineCoverLoaded) { + if (!hasCustomDefault) { + Icon( + Icons.Default.Book, + contentDescription = null, + tint = LegadoTheme.colorScheme.secondary, + modifier = Modifier + .fillMaxSize(0.35f) + .align(Alignment.Center) + ) + } + CoverTextOverlay( + name = name, + author = author, + isNight = isNight ) } - CoverTextOverlay( - name = name, - author = author, - isNight = isNight - ) } } } +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +private fun rememberSharedCoverTransitionRadius( + sharedCoverKey: String?, + radius: Dp, + animatedVisibilityScope: AnimatedVisibilityScope? +): Dp { + if (sharedCoverKey == null || animatedVisibilityScope == null) { + return radius + } + + val transition = animatedVisibilityScope.transition + val startRadius = sharedCoverRadiusCache[sharedCoverKey] ?: radius + val animatedRadiusValue by transition.animateFloat( + label = "book-cover-corner-radius" + ) { state -> + if (state == EnterExitState.Visible) radius.value else startRadius.value + } + + LaunchedEffect( + sharedCoverKey, + radius, + transition.currentState, + transition.targetState + ) { + if ( + transition.currentState == EnterExitState.Visible && + transition.targetState == EnterExitState.Visible + ) { + sharedCoverRadiusCache[sharedCoverKey] = radius + if (sharedCoverRadiusCache.size > SharedCoverRadiusCacheMaxSize) { + sharedCoverRadiusCache.keys + .firstOrNull { it != sharedCoverKey } + ?.let(sharedCoverRadiusCache::remove) + } + } + } + + return animatedRadiusValue.dp +} + @Composable private fun CoverTextOverlay( name: String?,