修复共享元素动画首帧为上一个书籍封面的问题

This commit is contained in:
HapeLee
2026-05-23 01:34:53 +08:00
parent 7e35177bac
commit f6ee759fc2
@@ -7,8 +7,10 @@ import android.text.StaticLayout
import android.text.TextPaint import android.text.TextPaint
import android.text.TextUtils import android.text.TextUtils
import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.animateFloat
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@@ -25,6 +27,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -47,6 +50,9 @@ import io.legado.app.ui.theme.LegadoTheme
import org.koin.compose.koinInject import org.koin.compose.koinInject
import io.legado.app.model.BookCover as BookCoverModel import io.legado.app.model.BookCover as BookCoverModel
private const val SharedCoverRadiusCacheMaxSize = 256
private val sharedCoverRadiusCache = mutableStateMapOf<String, Dp>()
@OptIn(ExperimentalSharedTransitionApi::class) @OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun CoilBookCover( fun CoilBookCover(
@@ -81,107 +87,152 @@ fun CoilBookCover(
mutableStateOf(sharedCoverKey != null && finalPath != null) mutableStateOf(sharedCoverKey != null && finalPath != null)
} }
Box( val transitionRadius = rememberSharedCoverTransitionRadius(
modifier = modifier sharedCoverKey = sharedCoverKey,
.aspectRatio(5f / 7f) radius = radius,
.then( animatedVisibilityScope = animatedVisibilityScope
with(sharedTransitionScope) { )
if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) { val shape = remember(transitionRadius) { RoundedCornerShape(transitionRadius) }
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))
)
}
}
if (finalPath != null) { key(path, sharedCoverKey) {
key(finalPath) { Box(
AsyncImage( modifier = modifier
model = buildCoverImageRequest( .aspectRatio(5f / 7f)
context = context, .then(
data = finalPath, with(sharedTransitionScope) {
sourceOrigin = sourceOrigin, if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) {
loadOnlyWifi = CoverConfig.loadCoverOnlyWifi, Modifier.sharedElement(
crossfade = showLoadingPlaceholder, sharedContentState = rememberSharedContentState(sharedCoverKey),
memoryCacheKey = finalPath, animatedVisibilityScope = animatedVisibilityScope,
), clipInOverlayDuringTransition = OverlayClip(shape)
contentDescription = null, )
imageLoader = koinInject(), } else Modifier
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(4.dp)),
onSuccess = {
isOnlineCoverLoaded = true
onLoadFinish?.invoke()
},
onError = {
isOnlineCoverLoaded = false
onLoadFinish?.invoke()
} }
) )
} .then(
} else { if (CoverConfig.coverShowShadow) {
LaunchedEffect(Unit) { Modifier.shadow(4.dp, shape)
onLoadFinish?.invoke() } 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 (finalPath != null) {
if (!hasCustomDefault) { AsyncImage(
Icon( model = buildCoverImageRequest(
Icons.Default.Book, context = context,
contentDescription = null, data = finalPath,
tint = LegadoTheme.colorScheme.secondary, sourceOrigin = sourceOrigin,
modifier = Modifier loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
.fillMaxSize(0.35f) crossfade = showLoadingPlaceholder,
.align(Alignment.Center) 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 @Composable
private fun CoverTextOverlay( private fun CoverTextOverlay(
name: String?, name: String?,