identity: add the shared content transitions

expandEnter/collapseExit (revealable sections), itemEnter (resolved content),
animateItemMotion (lazy-list relayout) and fadeThrough — all driven by the M3
Expressive motion scheme and honouring rememberReduceMotion. Drawn from
Calendula's motion helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 15:13:06 +02:00
parent 2085b82a5e
commit ea96e970c7
3 changed files with 93 additions and 0 deletions

View File

@@ -36,6 +36,7 @@ dependencies {
implementation(libs.androidx.ui)
implementation(libs.androidx.foundation)
implementation(libs.androidx.material3)
implementation(libs.androidx.animation)
// activity-compose for PredictiveBackHandler + BackEventCompat (the back-gesture peek).
implementation(libs.androidx.activity.compose)
}

View File

@@ -0,0 +1,91 @@
package de.jeanlucmakiola.floret.identity
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
* The family's content transitions, driven by the active M3 Expressive motion
* scheme so every app reveals, collapses and re-lays-out the same way. Each
* honours [rememberReduceMotion]: when the user has asked the system to remove
* animations, the spatial movement is dropped and only a quick fade remains.
*
* Drawn from Calendula's motion helpers; pair [expandEnter] with [collapseExit]
* on an `AnimatedVisibility`, use [itemEnter] for content revealed once it
* resolves, [animateItemMotion] for `LazyColumn`/`LazyRow` row relayout, and
* [fadeThrough] to swap whole blocks with no spatial direction.
*/
/**
* Enter for a vertically-revealed section (expandable rows, inline fields):
* height grows from the top while fading in; a quick fade only under reduced
* motion. Pair with [collapseExit].
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun expandEnter(reduceMotion: Boolean = rememberReduceMotion()): EnterTransition {
val fade = fadeIn(MaterialTheme.motionScheme.fastEffectsSpec())
return if (reduceMotion) fade else expandVertically(MaterialTheme.motionScheme.fastSpatialSpec()) + fade
}
/** Exit counterpart to [expandEnter]: shrink + fade, or fade only under reduced motion. */
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun collapseExit(reduceMotion: Boolean = rememberReduceMotion()): ExitTransition {
val fade = fadeOut(MaterialTheme.motionScheme.fastEffectsSpec())
return if (reduceMotion) fade else shrinkVertically(MaterialTheme.motionScheme.fastSpatialSpec()) + fade
}
/**
* Enter for content revealed by an `AnimatedContent`/`AnimatedVisibility` (e.g.
* results once a query resolves): a gentle rise + fade; fade only under reduced
* motion.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun itemEnter(reduceMotion: Boolean = rememberReduceMotion()): EnterTransition {
val fade = fadeIn(MaterialTheme.motionScheme.fastEffectsSpec())
return if (reduceMotion) fade else fade + slideInVertically(MaterialTheme.motionScheme.fastSpatialSpec()) { h -> h / 6 }
}
/**
* `LazyItemScope.animateItem` wired to the app's motion scheme instead of
* Compose's default spring, so list rows fade/relocate consistently. Returns a
* bare [Modifier] under reduced motion so rows snap into place. Requires stable
* item keys.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun LazyItemScope.animateItemMotion(reduceMotion: Boolean = rememberReduceMotion()): Modifier =
if (reduceMotion) {
Modifier
} else {
Modifier.animateItem(
fadeInSpec = MaterialTheme.motionScheme.fastEffectsSpec(),
placementSpec = MaterialTheme.motionScheme.fastSpatialSpec(),
fadeOutSpec = MaterialTheme.motionScheme.fastEffectsSpec(),
)
}
/**
* Cross-fade [ContentTransform] for swapping whole screens or blocks with no
* meaningful spatial direction (e.g. onboarding gates). Pure opacity, so it
* doubles as its own reduced-motion form.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun fadeThrough(): ContentTransform {
val fade = MaterialTheme.motionScheme.fastEffectsSpec<Float>()
return fadeIn(fade).togetherWith(fadeOut(fade))
}