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

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

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

+ 138 - 87
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<String, Dp>()
+
 @OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun CoilBookCover(
@@ -81,105 +87,150 @@ 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
-                        )
+    val transitionRadius = rememberSharedCoverTransitionRadius(
+        sharedCoverKey = sharedCoverKey,
+        radius = radius,
+        animatedVisibilityScope = animatedVisibilityScope
+    )
+    val shape = remember(transitionRadius) { RoundedCornerShape(transitionRadius) }
+
+    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
+                    }
+                )
+                .then(
+                    if (CoverConfig.coverShowShadow) {
+                        Modifier.shadow(4.dp, shape)
                     } 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))
                 )
-            }
-        }
+                .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 (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
+                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()
                     }
-                )
+                }
             }
-        } 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
+                )
             }
         }
+    }
+}
 
-        if (showLoadingPlaceholder && !isOnlineCoverLoaded) {
-            if (!hasCustomDefault) {
-                Icon(
-                    Icons.Default.Book,
-                    contentDescription = null,
-                    tint = LegadoTheme.colorScheme.secondary,
-                    modifier = Modifier
-                        .fillMaxSize(0.35f)
-                        .align(Alignment.Center)
-                )
+@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)
             }
-            CoverTextOverlay(
-                name = name,
-                author = author,
-                isNight = isNight
-            )
         }
     }
+
+    return animatedRadiusValue.dp
 }
 
 @Composable