修复共享元素动画首帧为上一个书籍封面的问题
This commit is contained in:
+142
-91
@@ -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?,
|
||||||
|
|||||||
Reference in New Issue
Block a user