Implement Shared Element Transition Connector Framework (#1614)
undefined
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user