diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarTransitions.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarTransitions.kt index 54a5b7a..cf57422 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarTransitions.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarTransitions.kt @@ -1,6 +1,7 @@ package de.jeanlucmakiola.calendula.ui.common import androidx.compose.animation.ContentTransform +import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -23,9 +24,13 @@ import androidx.compose.ui.unit.IntOffset */ /** - * The M3 Expressive spatial spring used for the month/week slide: the *fast* - * spring-physics spec from the active motion scheme — snappy with a subtle - * springy settle, rather than a fixed easing curve. + * The M3 Expressive spatial spring used for the month/week/day slide: the + * *default* spring-physics spec from the active motion scheme, rather than a + * fixed easing curve. + * + * Default rather than fast: `fastSpatialSpec` is tuned for small, incidental + * movement, and driving a whole calendar page with it made the settle read as a + * twitch instead of a glide. * * Read it in a composable scope (this helper) so it can be captured by the * non-composable `AnimatedContent` transitionSpec lambda. @@ -33,27 +38,34 @@ import androidx.compose.ui.unit.IntOffset @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun rememberCalendarSlideSpec(): FiniteAnimationSpec = - MaterialTheme.motionScheme.fastSpatialSpec() + MaterialTheme.motionScheme.defaultSpatialSpec() /** - * The fast effects spec from the active motion scheme, for opacity (fade) - * transitions. Captured in composable scope alongside [rememberCalendarSlideSpec] - * for use in non-composable transition lambdas and as the reduced-motion fallback. + * The effects spec from the active motion scheme, for the opacity half of the + * transition. Captured in composable scope alongside [rememberCalendarSlideSpec] + * for use in non-composable transition lambdas, and reused on its own as the + * reduced-motion fallback. + * + * Opacity gets its own spec on purpose: M3 Expressive springs *position* and + * eases *opacity*, and a fade that bounced with the slide would shimmer. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun rememberCalendarFadeSpec(): FiniteAnimationSpec = - MaterialTheme.motionScheme.fastEffectsSpec() + MaterialTheme.motionScheme.defaultEffectsSpec() /** - * Horizontal slide for navigating between adjacent months/weeks/days. + * Navigating between adjacent months/weeks/days, as M3's shared-axis X: the + * outgoing page slides and fades one way while the incoming one arrives from the + * other, position on a spring and opacity on an easing curve. * * @param slideDir +1 = forward (incoming from the right), -1 = back, 0 = jump * (e.g. "today"); a jump reuses the forward direction. * @param spec spatial animation spec, typically [rememberCalendarSlideSpec]. - * @param fadeSpec effects spec for the reduced-motion fade, typically + * @param fadeSpec effects spec for the opacity half, and for the whole + * transition under reduced motion; typically * [rememberCalendarFadeSpec]. - * @param reduceMotion when true, swap the directional slide for a plain cross-fade. + * @param reduceMotion when true, drop the movement and cross-fade alone. */ fun calendarSlideTransition( slideDir: Int, @@ -65,6 +77,26 @@ fun calendarSlideTransition( return fadeIn(fadeSpec).togetherWith(fadeOut(fadeSpec)) } val dir = if (slideDir == 0) 1 else slideDir - return slideInHorizontally(spec) { w -> dir * w } - .togetherWith(slideOutHorizontally(spec) { w -> -dir * w }) + return ContentTransform( + targetContentEnter = + slideInHorizontally(spec) { w -> dir * w / SLIDE_TRAVEL_DIVISOR } + fadeIn(fadeSpec), + initialContentExit = + slideOutHorizontally(spec) { w -> -dir * w / SLIDE_TRAVEL_DIVISOR } + fadeOut(fadeSpec), + // AnimatedContent clips to the animating container by default, which + // shears the pages against the viewport edge as they pass. There is no + // size change here to contain — both pages are the same grid. + sizeTransform = SizeTransform(clip = false), + ) } + +/** + * How far a page travels, as a fraction of the container width. + * + * A full width was the obvious reading of "paging", but the two pages are + * stacked and both opaque, so a full-width slide showed one grid racing across + * another — the movement carried the whole transition and had a long way to go. + * Under M3's shared-axis pattern the offset only has to *hint* the direction + * while the cross-fade does the swapping, so a fifth of the width is plenty and + * leaves nothing skating past. + */ +private const val SLIDE_TRAVEL_DIVISOR = 5 diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index 4ef64d3..7feb567 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -777,15 +777,20 @@ internal fun DenseMonthGrid( /** * The month-changing horizontal swipe, shared by the paged and split styles. - * Accumulates the drag and commits past a small threshold on release — the grid + * Accumulates the drag and commits past a threshold on release — the grid * doesn't follow the finger, so there is no distance to rubber-band against. + * + * The threshold matches the week and day views'. It used to be 6dp, which is + * inside the distance a tap wanders: brushing the grid changed the month, and a + * page that turns on an unintended gesture reads as the animation misfiring + * rather than as the gesture being over-eager. */ @Composable private fun rememberMonthSwipeModifier( onSwipeNext: () -> Unit, onSwipePrev: () -> Unit, ): Modifier { - val threshold = with(LocalDensity.current) { 6.dp.toPx() } + val threshold = with(LocalDensity.current) { MONTH_SWIPE_THRESHOLD.toPx() } var dragAccum by remember { mutableFloatStateOf(0f) } return Modifier.pointerInput(Unit) { detectHorizontalDragGestures( @@ -803,6 +808,9 @@ private fun rememberMonthSwipeModifier( } } +/** Drag distance that commits a month change, matching the week and day views. */ +private val MONTH_SWIPE_THRESHOLD = 24.dp + /** * Split style content: the compact grid keeps the month swipe, the pane below it * lists whatever day is selected.