Restore sequential hero copy and first-appearance entry
This commit is contained in:
parent
7bfa431350
commit
79f64f6e18
5 changed files with 226 additions and 53 deletions
|
|
@ -15,6 +15,8 @@ import androidx.compose.runtime.key
|
|||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -27,6 +29,9 @@ import androidx.compose.ui.semantics.semantics
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import org.justinvisser.wolfskin.design.WolfskinTokens.Motion as M
|
||||
|
||||
|
|
@ -55,11 +60,7 @@ private class HeroLayer<T>(val identity: Any, var target: T, initialAlpha: Float
|
|||
var outgoing by mutableStateOf(false)
|
||||
}
|
||||
|
||||
private class HeroCopyLayer<T>(val identity: Any, var target: T, initialAlpha: Float) {
|
||||
val alpha = Animatable(initialAlpha)
|
||||
val progress = Animatable(initialAlpha)
|
||||
var outgoing by mutableStateOf(false)
|
||||
}
|
||||
private class HeroCopyTarget<T>(val identity: Any, var target: T)
|
||||
|
||||
/** Keep the painted composite while loading; interruptions fade each retained layer from its current alpha. */
|
||||
@Composable
|
||||
|
|
@ -102,42 +103,59 @@ fun <T> HeroArtCrossfade(identity: Any, target: T, height: Dp, modifier: Modifie
|
|||
}
|
||||
}
|
||||
|
||||
/** Copy counter-fades after artwork is ready; outgoing blocks hold their current translation. */
|
||||
/** One copy slot fades through after artwork readiness; no two titles can be painted together. */
|
||||
@Composable
|
||||
fun <T> HeroCopyHandoff(identity: Any, target: T, modifier: Modifier = Modifier, content: @Composable (T) -> Unit) {
|
||||
val initial = remember { HeroCopyLayer(identity, target, 1f) }
|
||||
val layers = remember { mutableStateListOf(initial) }
|
||||
val selected = remember(identity) { layers.lastOrNull { it.identity == identity && !it.outgoing } ?: HeroCopyLayer(identity, target, 0f) }
|
||||
var entered by rememberSaveable { mutableStateOf(false) }
|
||||
val selected = remember(identity) { HeroCopyTarget(identity, target) }
|
||||
val currentSelected by rememberUpdatedState(selected)
|
||||
var displayed by remember { mutableStateOf(selected) }
|
||||
val alpha = remember { Animatable(if (entered) 1f else 0f) }
|
||||
val progress = remember { Animatable(if (entered) 1f else 0f) }
|
||||
var initialEntry by remember { mutableStateOf(!entered) }
|
||||
var handingOff by remember { mutableStateOf(false) }
|
||||
var outgoing by remember { mutableStateOf(false) }
|
||||
val reduced = reducedMotion()
|
||||
val presentation = LocalHeroPresentation.current
|
||||
val rise = with(LocalDensity.current) { M.copyRise.toPx() }
|
||||
LaunchedEffect(selected, reduced) {
|
||||
if (selected !in layers) {
|
||||
if (presentation != null) snapshotFlow { presentation.identity == identity }.first { it }
|
||||
layers.add(selected)
|
||||
layers.forEach { if (it !== selected) it.outgoing = true }
|
||||
}
|
||||
}
|
||||
Box(modifier, contentAlignment = Alignment.BottomStart) {
|
||||
layers.forEach { layer -> key(layer) {
|
||||
SideEffect { if (layer.identity == identity) layer.target = target }
|
||||
LaunchedEffect(layer.outgoing, reduced) {
|
||||
if (layer.outgoing) {
|
||||
layer.alpha.animateTo(0f, tween(motionDuration(M.heroCopyIn, reduced), easing = M.easeOut))
|
||||
layers.remove(layer)
|
||||
} else if (layer.alpha.value != 1f) coroutineScope {
|
||||
launch { layer.alpha.animateTo(1f, tween(motionDuration(M.heroCopyIn, reduced), easing = M.easeOut)) }
|
||||
launch { layer.progress.animateTo(1f, tween(motionDuration(M.heroCopyIn, reduced), easing = M.easeOut)) }
|
||||
val rise = with(LocalDensity.current) { (if (initialEntry) M.copyEntryRise else M.copyRise).toPx() }
|
||||
SideEffect { selected.target = target }
|
||||
LaunchedEffect(presentation, reduced) {
|
||||
snapshotFlow { currentSelected.takeIf { presentation == null || presentation.identity == it.identity } }
|
||||
.filterNotNull()
|
||||
.distinctUntilChangedBy { it.identity }
|
||||
.collectLatest { next ->
|
||||
if (!entered) {
|
||||
displayed = next
|
||||
entered = true
|
||||
} else if (displayed.identity != next.identity || handingOff) {
|
||||
handingOff = true
|
||||
outgoing = true
|
||||
coroutineScope {
|
||||
val fade = if (alpha.value > 0f) launch {
|
||||
alpha.animateTo(0f, tween(motionDuration(M.heroCopyOut, reduced), easing = M.ease))
|
||||
} else null
|
||||
if (!reduced) kotlinx.coroutines.delay(M.heroCopyDelay.toLong())
|
||||
fade?.join()
|
||||
}
|
||||
outgoing = false
|
||||
displayed = next
|
||||
initialEntry = false
|
||||
progress.snapTo(0f)
|
||||
}
|
||||
if (alpha.value != 1f || progress.value != 1f) coroutineScope {
|
||||
val duration = motionDuration(if (initialEntry) M.rowEnter else M.heroCopyIn, reduced)
|
||||
launch { alpha.animateTo(1f, tween(duration, easing = M.easeOut)) }
|
||||
launch { progress.animateTo(1f, tween(duration, easing = M.easeOut)) }
|
||||
}
|
||||
handingOff = false
|
||||
}
|
||||
Box(Modifier.graphicsLayer {
|
||||
alpha = layer.alpha.value
|
||||
translationY = if (reduced) 0f else rise * (1f - layer.progress.value)
|
||||
}.testTag(if (layer === layers.last()) "hero-copy" else "hero-copy-outgoing").semantics {
|
||||
this[HeroLayerAlpha] = layer.alpha.value
|
||||
this[HeroLayerTranslation] = if (reduced) 0f else rise * (1f - layer.progress.value)
|
||||
this[HeroLayerIdentity] = layer.identity.toString()
|
||||
}) { content(if (layer.identity == identity) target else layer.target) }
|
||||
} }
|
||||
}
|
||||
Box(modifier.graphicsLayer {
|
||||
this.alpha = alpha.value
|
||||
translationY = if (reduced) 0f else rise * (1f - progress.value)
|
||||
}.testTag(if (outgoing) "hero-copy-outgoing" else "hero-copy").semantics {
|
||||
this[HeroLayerAlpha] = alpha.value
|
||||
this[HeroLayerTranslation] = if (reduced) 0f else rise * (1f - progress.value)
|
||||
this[HeroLayerIdentity] = displayed.identity.toString()
|
||||
}) { content(if (displayed.identity == identity) target else displayed.target) }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue