Implement Shared Element Transition Connector Framework (#1614)

undefined
This commit is contained in:
renovate[bot] authored and GitHub committed 2024-09-09 21:46:07 +08:00
1 parent 2b4a6dc57a
commit bd5bd1e571
8 files changed
+146 -96

No files matched your search

@@ -55,6 +55,7 @@ import com.hippo.ehviewer.legacy.cleanObsoleteCache
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.lockObserver
import com.hippo.ehviewer.ui.tools.dataStateFlow
import com.hippo.ehviewer.ui.tools.initSETConnection
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.Crash
import com.hippo.ehviewer.util.FavouriteStatusRouter
@@ -83,6 +84,7 @@ class EhApplication :
Application(),
SingletonImageLoader.Factory {
override fun onCreate() {
initSETConnection()
// Initialize Settings on first access
lifecycleScope.launchIO {
val mode = Settings.theme
@@ -45,6 +45,7 @@ import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.GalleryListCardRating
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.listThumbGenerator
import com.hippo.ehviewer.util.FileUtils
private val ids = Tuple9(1, 2, 3, 4, 5, 6, 7, 8, 9)
@@ -108,10 +109,12 @@ fun DownloadCard(
Row {
val thumb = remember {
movableContentOf<DownloadInfo> {
EhAsyncCropThumb(
key = it,
modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(),
)
with(listThumbGenerator) {
EhAsyncCropThumb(
key = it,
modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(),
)
}
}
}
if (selectMode) {
@@ -17,7 +17,9 @@ import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.tools.SETNodeGenerator
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.sharedBounds
import com.hippo.ehviewer.ui.tools.shouldCrop
@Composable
@@ -27,7 +29,7 @@ fun requestOf(model: GalleryInfo): ImageRequest {
return remember(model) { context.imageRequest(model) }
}
context(SharedTransitionScope, TransitionsVisibilityScope)
context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
@Composable
@NonRestartableComposable
fun EhAsyncThumb(
@@ -38,17 +40,14 @@ fun EhAsyncThumb(
) = AsyncImage(
model = requestOf(model),
contentDescription = null,
modifier = modifier
// https://github.com/FooIbar/EhViewer/issues/1527
// .sharedBounds(key = "${model.gid}")
.clip(ShapeDefaults.Medium),
modifier = modifier.sharedBounds(key = "${model.gid}").clip(ShapeDefaults.Medium),
onSuccess = onSuccess?.let { callback ->
{ callback(it.result.image) }
},
contentScale = contentScale,
)
context(SharedTransitionScope, TransitionsVisibilityScope)
context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
@Composable
fun EhAsyncCropThumb(
key: GalleryInfo,
@@ -58,10 +57,7 @@ fun EhAsyncCropThumb(
AsyncImage(
model = requestOf(key),
contentDescription = null,
modifier = modifier
// https://github.com/FooIbar/EhViewer/issues/1527
// .sharedBounds(key = "${key.gid}")
.clip(ShapeDefaults.Medium),
modifier = modifier.sharedBounds(key = "${key.gid}").clip(ShapeDefaults.Medium),
onSuccess = {
if (it.result.image.shouldCrop) {
contentScale = ContentScale.Crop
@@ -37,6 +37,7 @@ import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.SadAndroid
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.detailThumbGenerator
@Composable
fun GalleryDetailHeaderInfoCard(
@@ -109,13 +110,15 @@ fun GalleryDetailHeaderCard(
) = ElevatedCard(modifier = modifier) {
Row {
Card {
EhAsyncCropThumb(
key = remember(info.gid) { info },
modifier = Modifier.size(
dimensionResource(id = R.dimen.gallery_detail_thumb_width),
dimensionResource(id = R.dimen.gallery_detail_thumb_height),
),
)
with(detailThumbGenerator) {
EhAsyncCropThumb(
key = remember(info.gid) { info },
modifier = Modifier.size(
dimensionResource(id = R.dimen.gallery_detail_thumb_width),
dimensionResource(id = R.dimen.gallery_detail_thumb_height),
),
)
}
}
Spacer(modifier = Modifier.weight(0.5F))
Column(
@@ -53,6 +53,7 @@ import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.ElevatedCard
import com.hippo.ehviewer.ui.tools.GalleryListCardRating
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.listThumbGenerator
import com.hippo.ehviewer.util.FavouriteStatusRouter
private val ids = Tuple7(1, 2, 3, 4, 5, 6, 7)
@@ -108,10 +109,12 @@ fun GalleryInfoListItem(
) {
Row {
Card {
EhAsyncCropThumb(
key = info,
modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(),
)
with(listThumbGenerator) {
EhAsyncCropThumb(
key = info,
modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(),
)
}
}
ConstraintLayout(
modifier = Modifier.padding(start = 8.dp, top = 2.dp, end = 4.dp, bottom = 4.dp).fillMaxSize(),
@@ -227,13 +230,15 @@ fun GalleryInfoGridItem(
}
mutableFloatStateOf(ratio)
}
EhAsyncThumb(
model = info,
modifier = Modifier.aspectRatio(ratio),
onSuccess = {
ratio = (it.width.toFloat() / it.height).coerceIn(MIN_RATIO, MAX_RATIO)
},
)
with(listThumbGenerator) {
EhAsyncThumb(
model = info,
modifier = Modifier.aspectRatio(ratio),
onSuccess = {
ratio = (it.width.toFloat() / it.height).coerceIn(MIN_RATIO, MAX_RATIO)
},
)
}
val categoryColor = EhUtils.getCategoryColor(info.category)
Badge(
modifier = Modifier.align(Alignment.TopEnd).widthIn(min = 32.dp).height(24.dp),
@@ -148,7 +148,6 @@ import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.awaitActivityResult
import com.hippo.ehviewer.util.bgWork
import com.hippo.ehviewer.util.displayString
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.requestPermission
import com.hippo.files.delete
@@ -220,12 +219,11 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
}
val (gid, token) = remember {
when (args) {
is GalleryInfoArgs -> args.galleryInfo.run { gid to token }
is GalleryInfoArgs -> with(args.galleryInfo) { gid to token }
is TokenArgs -> args.gid to args.token
}
}
val galleryDetailUrl = remember { EhUrl.getGalleryDetailUrl(gid, token, 0, false) }
val activity = remember { findActivity<MainActivity>() }
var showReadAction by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(args, galleryInfo) {
if (showReadAction) {
@@ -244,9 +242,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
}
}
}
with(activity) {
ProvideAssistContent(galleryDetailUrl)
}
ProvideAssistContent(galleryDetailUrl)
var getDetailError by rememberSaveable { mutableStateOf("") }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
@@ -330,7 +326,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
}
val name = "$gid-${EhUtils.getSuitableTitle(galleryDetail)}.zip"
try {
activity.startActivity(intent)
startActivity(intent)
withUIContext { addTextToClipboard(name, true) }
} catch (_: ActivityNotFoundException) {
val r = DownloadManager.Request(uri)
@@ -712,28 +708,26 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
}
launchIO {
awaitSelectAction {
with(activity) {
onSelect(copy) {
addTextToClipboard(tag)
}
if (temp != translated) {
onSelect(copyTrans) {
addTextToClipboard(translated)
}
}
onSelect(showDefine) {
openBrowser(EhUrl.getTagDefinitionUrl(temp))
}
onSelect(addFilter) {
awaitPermissionOrCancel { Text(text = stringResource(R.string.filter_the_tag, tag)) }
Filter(FilterMode.TAG, tag).remember()
showSnackbar(filterAdded)
}
if (galleryDetail.apiUid >= 0) {
onSelect(upTag) { galleryDetail.voteTag(tag, 1) }
onSelect(downTag) { galleryDetail.voteTag(tag, -1) }
onSelect(copy) {
addTextToClipboard(tag)
}
if (temp != translated) {
onSelect(copyTrans) {
addTextToClipboard(translated)
}
}
onSelect(showDefine) {
openBrowser(EhUrl.getTagDefinitionUrl(temp))
}
onSelect(addFilter) {
awaitPermissionOrCancel { Text(text = stringResource(R.string.filter_the_tag, tag)) }
Filter(FilterMode.TAG, tag).remember()
showSnackbar(filterAdded)
}
if (galleryDetail.apiUid >= 0) {
onSelect(upTag) { galleryDetail.voteTag(tag, 1) }
onSelect(downTag) { galleryDetail.voteTag(tag, -1) }
}
}()
}
},
@@ -832,7 +826,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
val onDownloadButtonClick = rememberLambda(galleryInfo) {
galleryDetail ?: return@rememberLambda
if (EhDownloadManager.getDownloadState(galleryDetail.gid) == DownloadInfo.STATE_INVALID) {
launchUI { startDownload(activity, false, galleryDetail.galleryInfo) }
launchUI { startDownload(implicit<MainActivity>(), false, galleryDetail.galleryInfo) }
} else {
launch { confirmRemoveDownload(galleryDetail) }
}
@@ -1000,7 +994,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
}
IconButton(
onClick = {
AppHelper.share(activity, galleryDetailUrl)
AppHelper.share(implicit<MainActivity>(), galleryDetailUrl)
// In case the link is copied to the clipboard
Settings.clipboardTextHashCode = galleryDetailUrl.hashCode()
},
@@ -1,41 +1,6 @@
package com.hippo.ehviewer.ui.tools
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
inline fun Modifier.thenIf(condition: Boolean, crossinline block: Modifier.() -> Modifier) =
if (condition) block() else this
val NoopTransitionsVisibilityScope = TransitionsVisibilityScope(emptySet())
@JvmInline
@Stable
value class TransitionsVisibilityScope(val scopes: Set<AnimatedVisibilityScope>)
context(TransitionsVisibilityScope)
@Composable
inline fun <T> togetherWith(scope: AnimatedVisibilityScope, block: @Composable TransitionsVisibilityScope.() -> T) =
block(remember(scopes, scope) { TransitionsVisibilityScope(scopes + scope) })
context(SharedTransitionScope, TransitionsVisibilityScope)
@Composable
fun Modifier.sharedBounds(
key: String,
enter: EnterTransition = fadeIn(),
exit: ExitTransition = fadeOut(),
) = scopes.fold(this) { modifier, scope ->
modifier.sharedBounds(
rememberSharedContentState("$key + ${scope.transition.label}"),
scope,
enter,
exit,
)
}
@@ -0,0 +1,82 @@
package com.hippo.ehviewer.ui.tools
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import arrow.atomic.AtomicInt
val NoopTransitionsVisibilityScope = TransitionsVisibilityScope(emptySet())
@JvmInline
@Stable
value class TransitionsVisibilityScope(val scopes: Set<AnimatedVisibilityScope>)
context(TransitionsVisibilityScope)
@Composable
inline fun <T> togetherWith(scope: AnimatedVisibilityScope, block: @Composable TransitionsVisibilityScope.() -> T) =
block(remember(scopes, scope) { TransitionsVisibilityScope(scopes + scope) })
context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator)
@Composable
fun Modifier.sharedBounds(
key: String,
enter: EnterTransition = fadeIn(),
exit: ExitTransition = fadeOut(),
) = remember(key) { summon(key) }.let { node ->
DisposableEffect(key) {
onDispose {
dispose(node)
}
}
scopes.fold(this) { modifier, scope ->
modifier.sharedBounds(
rememberSharedContentState("${node.syntheticKey} + ${scope.transition.label}"),
scope,
enter,
exit,
)
}
}
data class SETNode(
val contentKey: String,
val uniqueID: String,
)
val SETNode.syntheticKey: String
get() = contentKey + uniqueID
val listThumbGenerator = SETNodeGenerator()
val detailThumbGenerator = SETNodeGenerator()
fun initSETConnection() {
listThumbGenerator connectTo detailThumbGenerator
}
private val atomicIncId = AtomicInt()
class SETNodeGenerator {
private val tracker = hashMapOf<String, SETNode>()
private val opposites = mutableListOf<SETNodeGenerator>()
fun summon(contentKey: String): SETNode {
val node = opposites.firstNotNullOfOrNull { it.tracker[contentKey] } ?: SETNode(contentKey = contentKey, uniqueID = "${atomicIncId.getAndIncrement()}")
tracker[contentKey] = node
return node
}
fun dispose(node: SETNode) = tracker.remove(node.contentKey, node)
infix fun connectTo(other: SETNodeGenerator) {
opposites.add(other)
other.opposites.add(this)
}
}