feat(ui): consistent motion polish, predictive back & reduced-motion support
Centralise the app's motion vocabulary in CalendarTransitions.kt and route every surface through it so animation is consistent app-wide: - Shared expand/collapse, list-item and fade-through helpers; the event-edit expand pattern is now the shared one (no duplicate). - Settings reminder-override rows and the reminder Custom field now expand/ collapse instead of bare-fading. - Search and agenda rows animate (fade/relocate) via animateItem. - Month/week/day slide and the onboarding gates honour the new helpers. - View switching (month/week/day/agenda) now fades through instead of snapping — lateral navigation per M3, while in-view paging keeps its slide. Add full predictive-back support: - enableOnBackInvokedCallback in the manifest. - New Modifier.predictiveBack(onBack) drives the standard preview transform (scale/shift/round) following the back gesture; applied to detail, edit, search, settings (+ sub-screens & calendar manager via CollapsingScaffold), the calendar editor and import — each keeping its existing back semantics. Reduced-motion guardrail throughout: rememberReduceMotion() (reads the OS "remove animations" setting, which Compose ignores by default) collapses spatial motion to a quick fade and skips the back preview. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".CalendulaApp"
|
android:name=".CalendulaApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui
|
package de.jeanlucmakiola.calendula.ui
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -23,6 +24,7 @@ import de.jeanlucmakiola.calendula.domain.EventForm
|
|||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaScreen
|
import de.jeanlucmakiola.calendula.ui.agenda.AgendaScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.calendars.CalendarsScreen
|
import de.jeanlucmakiola.calendula.ui.calendars.CalendarsScreen
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.calendarFadeThrough
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
import de.jeanlucmakiola.calendula.ui.common.drillToDay
|
import de.jeanlucmakiola.calendula.ui.common.drillToDay
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
@@ -239,40 +241,50 @@ fun CalendarHost(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
when (view) {
|
// Switching between the peer views (month/week/day/agenda) is lateral
|
||||||
CalendarView.Week -> WeekScreen(
|
// navigation, so it fades through rather than sliding — paging *within* a
|
||||||
selectedView = view,
|
// view keeps the directional slide. AnimatedContent keyed on the view type.
|
||||||
onSelectView = onSelectView,
|
val viewSwitch = calendarFadeThrough()
|
||||||
onEventClick = onEventClick,
|
AnimatedContent(
|
||||||
onOpenSettings = onOpenSettings,
|
targetState = view,
|
||||||
onOpenSearch = onOpenSearch,
|
transitionSpec = { viewSwitch },
|
||||||
onCreateEvent = onCreateEvent,
|
label = "view-switch",
|
||||||
)
|
) { currentView ->
|
||||||
CalendarView.Day -> DayScreen(
|
when (currentView) {
|
||||||
selectedView = view,
|
CalendarView.Week -> WeekScreen(
|
||||||
onSelectView = onSelectView,
|
selectedView = currentView,
|
||||||
onEventClick = onEventClick,
|
onSelectView = onSelectView,
|
||||||
onOpenSettings = onOpenSettings,
|
onEventClick = onEventClick,
|
||||||
onOpenSearch = onOpenSearch,
|
onOpenSettings = onOpenSettings,
|
||||||
onCreateEvent = onCreateEvent,
|
onOpenSearch = onOpenSearch,
|
||||||
initialDateIso = pendingDayIso,
|
onCreateEvent = onCreateEvent,
|
||||||
)
|
)
|
||||||
CalendarView.Month -> MonthScreen(
|
CalendarView.Day -> DayScreen(
|
||||||
selectedView = view,
|
selectedView = currentView,
|
||||||
onSelectView = onSelectView,
|
onSelectView = onSelectView,
|
||||||
onOpenDay = onOpenDay,
|
onEventClick = onEventClick,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
onOpenSearch = onOpenSearch,
|
onOpenSearch = onOpenSearch,
|
||||||
onCreateEvent = onCreateEvent,
|
onCreateEvent = onCreateEvent,
|
||||||
)
|
initialDateIso = pendingDayIso,
|
||||||
CalendarView.Agenda -> AgendaScreen(
|
)
|
||||||
selectedView = view,
|
CalendarView.Month -> MonthScreen(
|
||||||
onSelectView = onSelectView,
|
selectedView = currentView,
|
||||||
onEventClick = onEventClick,
|
onSelectView = onSelectView,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenDay = onOpenDay,
|
||||||
onOpenSearch = onOpenSearch,
|
onOpenSettings = onOpenSettings,
|
||||||
onCreateEvent = onCreateEvent,
|
onOpenSearch = onOpenSearch,
|
||||||
)
|
onCreateEvent = onCreateEvent,
|
||||||
|
)
|
||||||
|
CalendarView.Agenda -> AgendaScreen(
|
||||||
|
selectedView = currentView,
|
||||||
|
onSelectView = onSelectView,
|
||||||
|
onEventClick = onEventClick,
|
||||||
|
onOpenSettings = onOpenSettings,
|
||||||
|
onOpenSearch = onOpenSearch,
|
||||||
|
onCreateEvent = onCreateEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search overlay — below detail/edit in the Box so a tapped result's
|
// Search overlay — below detail/edit in the Box so a tapped result's
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package de.jeanlucmakiola.calendula.ui
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -20,6 +23,7 @@ import de.jeanlucmakiola.calendula.ui.permission.PermissionScreen
|
|||||||
import de.jeanlucmakiola.calendula.ui.permission.ReminderOnboardingScreen
|
import de.jeanlucmakiola.calendula.ui.permission.ReminderOnboardingScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.permission.ReminderOnboardingViewModel
|
import de.jeanlucmakiola.calendula.ui.permission.ReminderOnboardingViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RootScreen(
|
fun RootScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -51,32 +55,40 @@ fun RootScreen(
|
|||||||
onDispose { lifecycle.removeObserver(obs) }
|
onDispose { lifecycle.removeObserver(obs) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasPermission) {
|
// Cross-fade the one-time onboarding gates so granting permission / finishing
|
||||||
// Second onboarding gate (v1.4, one-time): reminder notifications.
|
// onboarding eases into the next screen instead of snapping. A fade carries no
|
||||||
// Null until DataStore's first emission — render nothing for that
|
// spatial motion, so it stays appropriate under "remove animations" too.
|
||||||
// frame instead of flashing the wrong screen.
|
val gateSpec = MaterialTheme.motionScheme.fastEffectsSpec<Float>()
|
||||||
val reminderOnboarding: ReminderOnboardingViewModel = hiltViewModel()
|
Crossfade(targetState = hasPermission, animationSpec = gateSpec, label = "permissionGate") { granted ->
|
||||||
val onboardingDone by reminderOnboarding.onboardingDone.collectAsStateWithLifecycle()
|
if (granted) {
|
||||||
when (onboardingDone) {
|
// Second onboarding gate (v1.4, one-time): reminder notifications.
|
||||||
true -> CalendarHost(
|
// Null until DataStore's first emission — render nothing for that
|
||||||
modifier = modifier,
|
// frame instead of flashing the wrong screen.
|
||||||
requestedDetailKey = requestedDetailKey,
|
val reminderOnboarding: ReminderOnboardingViewModel = hiltViewModel()
|
||||||
onDetailKeyConsumed = onDetailKeyConsumed,
|
val onboardingDone by reminderOnboarding.onboardingDone.collectAsStateWithLifecycle()
|
||||||
widgetNavRequest = widgetNavRequest,
|
Crossfade(targetState = onboardingDone, animationSpec = gateSpec, label = "onboardingGate") { done ->
|
||||||
onWidgetNavConsumed = onWidgetNavConsumed,
|
when (done) {
|
||||||
requestedImportUri = requestedImportUri,
|
true -> CalendarHost(
|
||||||
onImportConsumed = onImportConsumed,
|
modifier = modifier,
|
||||||
)
|
requestedDetailKey = requestedDetailKey,
|
||||||
false -> ReminderOnboardingScreen(
|
onDetailKeyConsumed = onDetailKeyConsumed,
|
||||||
onFinished = reminderOnboarding::finish,
|
widgetNavRequest = widgetNavRequest,
|
||||||
|
onWidgetNavConsumed = onWidgetNavConsumed,
|
||||||
|
requestedImportUri = requestedImportUri,
|
||||||
|
onImportConsumed = onImportConsumed,
|
||||||
|
)
|
||||||
|
false -> ReminderOnboardingScreen(
|
||||||
|
onFinished = reminderOnboarding::finish,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PermissionScreen(
|
||||||
|
onGranted = { hasPermission = true },
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
null -> {}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
PermissionScreen(
|
|
||||||
onGranted = { hasPermission = true },
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.calendarAnimateItem
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||||
@@ -190,6 +191,7 @@ private fun AgendaList(
|
|||||||
AgendaEventRow(
|
AgendaEventRow(
|
||||||
event = event,
|
event = event,
|
||||||
position = positionOf(index, day.events.size),
|
position = positionOf(index, day.events.size),
|
||||||
|
modifier = calendarAnimateItem(),
|
||||||
onClick = { onEventClick(event) },
|
onClick = { onEventClick(event) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -221,11 +223,13 @@ private fun AgendaDayHeader(date: LocalDate, today: LocalDate) {
|
|||||||
private fun AgendaEventRow(
|
private fun AgendaEventRow(
|
||||||
event: EventInstance,
|
event: EventInstance,
|
||||||
position: Position,
|
position: Position,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
|
modifier = modifier,
|
||||||
title = title,
|
title = title,
|
||||||
summary = agendaTimeSummary(event),
|
summary = agendaTimeSummary(event),
|
||||||
position = position,
|
position = position,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.accounts.AccountManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -73,6 +72,7 @@ import de.jeanlucmakiola.calendula.R
|
|||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE
|
import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold
|
import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
||||||
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
||||||
@@ -295,10 +295,9 @@ private fun CalendarEditor(
|
|||||||
var confirmDelete by remember { mutableStateOf(false) }
|
var confirmDelete by remember { mutableStateOf(false) }
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
|
|
||||||
BackHandler(onBack = onClose)
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.predictiveBack(onBack = onClose)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface),
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|||||||
@@ -1,14 +1,56 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.activity.BackEventCompat
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user has asked the system to remove animations (Settings →
|
||||||
|
* Accessibility → "Remove animations", which sets the global animator duration
|
||||||
|
* scale to 0). Compose animations do *not* honour this platform flag on their
|
||||||
|
* own, so the shared motion helpers in this file check it and fall back to a
|
||||||
|
* quick cross-fade — opacity only, no spatial movement — to respect the
|
||||||
|
* vestibular intent of the setting while keeping state changes legible.
|
||||||
|
*
|
||||||
|
* Read once at composition; the scale changes rarely and only takes full effect
|
||||||
|
* after a process restart anyway.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberReduceMotion(): Boolean {
|
||||||
|
val resolver = LocalContext.current.contentResolver
|
||||||
|
return remember(resolver) {
|
||||||
|
Settings.Global.getFloat(resolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The M3 Expressive spatial spring used for the month/week slide: the *fast*
|
* The M3 Expressive spatial spring used for the month/week slide: the *fast*
|
||||||
@@ -24,17 +66,160 @@ fun rememberCalendarSlideSpec(): FiniteAnimationSpec<IntOffset> =
|
|||||||
MaterialTheme.motionScheme.fastSpatialSpec()
|
MaterialTheme.motionScheme.fastSpatialSpec()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Horizontal slide for navigating between adjacent months/weeks.
|
* 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.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun rememberCalendarFadeSpec(): FiniteAnimationSpec<Float> =
|
||||||
|
MaterialTheme.motionScheme.fastEffectsSpec()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal slide for navigating between adjacent months/weeks/days.
|
||||||
*
|
*
|
||||||
* @param slideDir +1 = forward (incoming from the right), -1 = back, 0 = jump
|
* @param slideDir +1 = forward (incoming from the right), -1 = back, 0 = jump
|
||||||
* (e.g. "today"); a jump reuses the forward direction.
|
* (e.g. "today"); a jump reuses the forward direction.
|
||||||
* @param spec spatial animation spec, typically [rememberCalendarSlideSpec].
|
* @param spec spatial animation spec, typically [rememberCalendarSlideSpec].
|
||||||
|
* @param fadeSpec effects spec for the reduced-motion fade, typically
|
||||||
|
* [rememberCalendarFadeSpec].
|
||||||
|
* @param reduceMotion when true, swap the directional slide for a plain cross-fade.
|
||||||
*/
|
*/
|
||||||
fun calendarSlideTransition(
|
fun calendarSlideTransition(
|
||||||
slideDir: Int,
|
slideDir: Int,
|
||||||
spec: FiniteAnimationSpec<IntOffset>,
|
spec: FiniteAnimationSpec<IntOffset>,
|
||||||
|
fadeSpec: FiniteAnimationSpec<Float>,
|
||||||
|
reduceMotion: Boolean,
|
||||||
): ContentTransform {
|
): ContentTransform {
|
||||||
|
if (reduceMotion) {
|
||||||
|
return fadeIn(fadeSpec).togetherWith(fadeOut(fadeSpec))
|
||||||
|
}
|
||||||
val dir = if (slideDir == 0) 1 else slideDir
|
val dir = if (slideDir == 0) 1 else slideDir
|
||||||
return slideInHorizontally(spec) { w -> dir * w }
|
return slideInHorizontally(spec) { w -> dir * w }
|
||||||
.togetherWith(slideOutHorizontally(spec) { w -> -dir * w })
|
.togetherWith(slideOutHorizontally(spec) { w -> -dir * w })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-fade [ContentTransform] for swapping whole screens or content blocks
|
||||||
|
* where there is no meaningful spatial direction (e.g. onboarding gates). Pure
|
||||||
|
* opacity, so it doubles as its own reduced-motion form.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun calendarFadeThrough(): ContentTransform {
|
||||||
|
val fade = MaterialTheme.motionScheme.fastEffectsSpec<Float>()
|
||||||
|
return fadeIn(fade).togetherWith(fadeOut(fade))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enter transition for a vertically-revealed section (expandable rows, inline
|
||||||
|
* fields): height grows from the top while fading in. Under reduced motion the
|
||||||
|
* height growth is dropped, leaving a quick fade.
|
||||||
|
*
|
||||||
|
* Pair with [calendarCollapseExit]. This is the promoted form of the pattern
|
||||||
|
* originally inlined in the event edit form, so every expandable surface in the
|
||||||
|
* app reveals the same way.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun calendarExpandEnter(reduceMotion: Boolean = rememberReduceMotion()): EnterTransition {
|
||||||
|
val fade = fadeIn(MaterialTheme.motionScheme.fastEffectsSpec())
|
||||||
|
return if (reduceMotion) {
|
||||||
|
fade
|
||||||
|
} else {
|
||||||
|
expandVertically(MaterialTheme.motionScheme.fastSpatialSpec()) + fade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exit counterpart to [calendarExpandEnter]: shrink + fade, or fade only under reduced motion. */
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun calendarCollapseExit(reduceMotion: Boolean = rememberReduceMotion()): ExitTransition {
|
||||||
|
val fade = fadeOut(MaterialTheme.motionScheme.fastEffectsSpec())
|
||||||
|
return if (reduceMotion) {
|
||||||
|
fade
|
||||||
|
} else {
|
||||||
|
shrinkVertically(MaterialTheme.motionScheme.fastSpatialSpec()) + fade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enter transition for content revealed by an `AnimatedContent`/`AnimatedVisibility`
|
||||||
|
* (e.g. search results once a query resolves): a gentle rise + fade. Reduced
|
||||||
|
* motion keeps the fade only.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun calendarItemEnter(reduceMotion: Boolean = rememberReduceMotion()): EnterTransition {
|
||||||
|
val fade = fadeIn(MaterialTheme.motionScheme.fastEffectsSpec())
|
||||||
|
return if (reduceMotion) {
|
||||||
|
fade
|
||||||
|
} else {
|
||||||
|
fade + slideInVertically(MaterialTheme.motionScheme.fastSpatialSpec()) { h -> h / 6 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared [LazyItemScope.animateItem] wiring so list rows fade/relocate with the
|
||||||
|
* app's motion scheme instead of Compose's default spring. Returns a bare
|
||||||
|
* [Modifier] under reduced motion so rows snap into place. Requires the list to
|
||||||
|
* supply stable item keys.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun LazyItemScope.calendarAnimateItem(reduceMotion: Boolean = rememberReduceMotion()): Modifier =
|
||||||
|
if (reduceMotion) {
|
||||||
|
Modifier
|
||||||
|
} else {
|
||||||
|
Modifier.animateItem(
|
||||||
|
fadeInSpec = MaterialTheme.motionScheme.fastEffectsSpec(),
|
||||||
|
placementSpec = MaterialTheme.motionScheme.fastSpatialSpec(),
|
||||||
|
fadeOutSpec = MaterialTheme.motionScheme.fastEffectsSpec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard Android predictive-back transform for a full-screen overlay: as the
|
||||||
|
* back gesture is dragged, the surface scales toward ~90%, shifts toward the
|
||||||
|
* swiped edge and rounds its corners, previewing what's behind. Completing the
|
||||||
|
* gesture invokes [onBack]; cancelling springs it back.
|
||||||
|
*
|
||||||
|
* A drop-in replacement for a screen's own `BackHandler(onBack)` — register it
|
||||||
|
* once and apply the returned [Modifier] to that screen's root so the preview
|
||||||
|
* respects the same back semantics. Under reduced motion the visual preview is
|
||||||
|
* skipped (the back still works); on API < 34 the system delivers no progress,
|
||||||
|
* so it degrades to a plain back.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Modifier.predictiveBack(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
reduceMotion: Boolean = rememberReduceMotion(),
|
||||||
|
): Modifier {
|
||||||
|
val progress = remember { Animatable(0f) }
|
||||||
|
var fromLeftEdge by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
PredictiveBackHandler(enabled = enabled) { events ->
|
||||||
|
try {
|
||||||
|
events.collect { event ->
|
||||||
|
fromLeftEdge = event.swipeEdge == BackEventCompat.EDGE_LEFT
|
||||||
|
progress.snapTo(FastOutSlowInEasing.transform(event.progress))
|
||||||
|
}
|
||||||
|
onBack()
|
||||||
|
progress.snapTo(0f)
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
progress.animateTo(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reduceMotion) return this
|
||||||
|
return this.graphicsLayer {
|
||||||
|
val p = progress.value
|
||||||
|
val scale = 1f - 0.1f * p
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
translationX = (if (fromLeftEdge) 1f else -1f) * 24.dp.toPx() * p
|
||||||
|
shape = RoundedCornerShape(32.dp.toPx() * p)
|
||||||
|
clip = p > 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -73,11 +72,11 @@ fun CollapsingScaffold(
|
|||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
BackHandler(onBack = onBack)
|
|
||||||
val scrollBehavior =
|
val scrollBehavior =
|
||||||
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.predictiveBack(onBack = onBack)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
|||||||
@@ -173,7 +173,11 @@ fun ReminderDefaultPicker(
|
|||||||
},
|
},
|
||||||
onClick = { customExpanded = !customExpanded },
|
onClick = { customExpanded = !customExpanded },
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = customExpanded) {
|
AnimatedVisibility(
|
||||||
|
visible = customExpanded,
|
||||||
|
enter = calendarExpandEnter(),
|
||||||
|
exit = calendarCollapseExit(),
|
||||||
|
) {
|
||||||
CustomReminderEditor(
|
CustomReminderEditor(
|
||||||
amountText = amountText,
|
amountText = amountText,
|
||||||
onAmountChange = { amountText = it },
|
onAmountChange = { amountText = it },
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberReduceMotion
|
||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
import de.jeanlucmakiola.calendula.ui.common.pastelize
|
import de.jeanlucmakiola.calendula.ui.common.pastelize
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
@@ -237,6 +239,8 @@ private fun DayContent(
|
|||||||
val threshold = with(density) { 24.dp.toPx() }
|
val threshold = with(density) { 24.dp.toPx() }
|
||||||
var dragAccum by remember { mutableFloatStateOf(0f) }
|
var dragAccum by remember { mutableFloatStateOf(0f) }
|
||||||
val slideSpec = rememberCalendarSlideSpec()
|
val slideSpec = rememberCalendarSlideSpec()
|
||||||
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
|
val reduceMotion = rememberReduceMotion()
|
||||||
|
|
||||||
// Hoisted above the per-day AnimatedContent so the vertical scroll position
|
// Hoisted above the per-day AnimatedContent so the vertical scroll position
|
||||||
// survives day-to-day swipes. We only centre on noon once, on first entry
|
// survives day-to-day swipes. We only centre on noon once, on first entry
|
||||||
@@ -287,7 +291,7 @@ private fun DayContent(
|
|||||||
DayUiState.Loading -> "loading"
|
DayUiState.Loading -> "loading"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transitionSpec = { calendarSlideTransition(slideDir, slideSpec) },
|
transitionSpec = { calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion) },
|
||||||
label = "day-transition",
|
label = "day-transition",
|
||||||
) { s ->
|
) { s ->
|
||||||
when (s) {
|
when (s) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -92,6 +91,7 @@ import de.jeanlucmakiola.calendula.domain.EventInstance
|
|||||||
import de.jeanlucmakiola.calendula.domain.EventStatus
|
import de.jeanlucmakiola.calendula.domain.EventStatus
|
||||||
import de.jeanlucmakiola.calendula.domain.RecurringWriteScope
|
import de.jeanlucmakiola.calendula.domain.RecurringWriteScope
|
||||||
import de.jeanlucmakiola.calendula.domain.Reminder
|
import de.jeanlucmakiola.calendula.domain.Reminder
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||||
import de.jeanlucmakiola.calendula.ui.common.OptionCard
|
import de.jeanlucmakiola.calendula.ui.common.OptionCard
|
||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
@@ -132,8 +132,6 @@ fun EventDetailScreen(
|
|||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val deleteState by viewModel.deleteState.collectAsStateWithLifecycle()
|
val deleteState by viewModel.deleteState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
BackHandler(onBack = onBack)
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
@@ -214,6 +212,7 @@ fun EventDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = Modifier.predictiveBack(onBack = onBack),
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|||||||
@@ -8,14 +8,9 @@ import android.content.pm.PackageManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.expandVertically
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -63,7 +58,6 @@ import androidx.compose.material3.DropdownMenu
|
|||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -121,6 +115,9 @@ import de.jeanlucmakiola.calendula.domain.RecurringWriteScope
|
|||||||
import de.jeanlucmakiola.calendula.domain.SimpleRecurrence
|
import de.jeanlucmakiola.calendula.domain.SimpleRecurrence
|
||||||
import de.jeanlucmakiola.calendula.domain.parseSimpleRecurrence
|
import de.jeanlucmakiola.calendula.domain.parseSimpleRecurrence
|
||||||
import de.jeanlucmakiola.calendula.domain.toRRule
|
import de.jeanlucmakiola.calendula.domain.toRRule
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE
|
import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog
|
import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
||||||
@@ -202,8 +199,6 @@ fun EventEditScreen(
|
|||||||
viewModel.reset()
|
viewModel.reset()
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
BackHandler(onBack = close)
|
|
||||||
|
|
||||||
// The event vanished between the detail screen and the edit tap — fall
|
// The event vanished between the detail screen and the edit tap — fall
|
||||||
// back to the detail screen, which shows its own failure state.
|
// back to the detail screen, which shows its own failure state.
|
||||||
LaunchedEffect(loadFailed) {
|
LaunchedEffect(loadFailed) {
|
||||||
@@ -253,6 +248,7 @@ fun EventEditScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = Modifier.predictiveBack(onBack = close),
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -410,7 +406,6 @@ private enum class PickerTarget { StartDate, StartTime, EndDate, EndTime }
|
|||||||
* spring open with the expressive spatial spec instead of popping in.
|
* spring open with the expressive spatial spec instead of popping in.
|
||||||
* (Initially-visible sections render without animating.)
|
* (Initially-visible sections render without animating.)
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OptionalFormSection(
|
private fun OptionalFormSection(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
@@ -418,10 +413,8 @@ private fun OptionalFormSection(
|
|||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = expandVertically(MaterialTheme.motionScheme.fastSpatialSpec()) +
|
enter = calendarExpandEnter(),
|
||||||
fadeIn(MaterialTheme.motionScheme.fastEffectsSpec()),
|
exit = calendarCollapseExit(),
|
||||||
exit = shrinkVertically(MaterialTheme.motionScheme.fastSpatialSpec()) +
|
|
||||||
fadeOut(MaterialTheme.motionScheme.fastEffectsSpec()),
|
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth(), content = content)
|
Column(modifier = Modifier.fillMaxWidth(), content = content)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.imports
|
package de.jeanlucmakiola.calendula.ui.imports
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -39,6 +38,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.domain.EventForm
|
import de.jeanlucmakiola.calendula.domain.EventForm
|
||||||
import de.jeanlucmakiola.calendula.domain.ics.IcsParseWarning
|
import de.jeanlucmakiola.calendula.domain.ics.IcsParseWarning
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
||||||
import de.jeanlucmakiola.calendula.ui.common.OptionCard
|
import de.jeanlucmakiola.calendula.ui.common.OptionCard
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +57,6 @@ fun ImportScreen(
|
|||||||
) {
|
) {
|
||||||
LaunchedEffect(uri) { viewModel.load(uri) }
|
LaunchedEffect(uri) { viewModel.load(uri) }
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
BackHandler(onBack = onClose)
|
|
||||||
|
|
||||||
// A single event isn't shown here — it opens the create form for review.
|
// A single event isn't shown here — it opens the create form for review.
|
||||||
LaunchedEffect(state) {
|
LaunchedEffect(state) {
|
||||||
@@ -65,7 +64,9 @@ fun ImportScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.predictiveBack(onBack = onClose)
|
||||||
|
.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.import_title)) },
|
title = { Text(stringResource(R.string.import_title)) },
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberReduceMotion
|
||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
@@ -217,6 +219,8 @@ private fun MonthContent(
|
|||||||
val threshold = with(density) { 6.dp.toPx() }
|
val threshold = with(density) { 6.dp.toPx() }
|
||||||
var dragAccum by remember { mutableFloatStateOf(0f) }
|
var dragAccum by remember { mutableFloatStateOf(0f) }
|
||||||
val slideSpec = rememberCalendarSlideSpec()
|
val slideSpec = rememberCalendarSlideSpec()
|
||||||
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
|
val reduceMotion = rememberReduceMotion()
|
||||||
|
|
||||||
val swipeModifier = Modifier.pointerInput(Unit) {
|
val swipeModifier = Modifier.pointerInput(Unit) {
|
||||||
detectHorizontalDragGestures(
|
detectHorizontalDragGestures(
|
||||||
@@ -243,7 +247,7 @@ private fun MonthContent(
|
|||||||
MonthUiState.Loading -> "loading"
|
MonthUiState.Loading -> "loading"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transitionSpec = { calendarSlideTransition(slideDir, slideSpec) },
|
transitionSpec = { calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion) },
|
||||||
label = "month-transition",
|
label = "month-transition",
|
||||||
) { s ->
|
) { s ->
|
||||||
when (s) {
|
when (s) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.search
|
package de.jeanlucmakiola.calendula.ui.search
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -49,6 +48,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.calendarAnimateItem
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
||||||
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
||||||
import de.jeanlucmakiola.calendula.ui.common.InlineTextField
|
import de.jeanlucmakiola.calendula.ui.common.InlineTextField
|
||||||
import de.jeanlucmakiola.calendula.ui.common.Position
|
import de.jeanlucmakiola.calendula.ui.common.Position
|
||||||
@@ -88,10 +89,8 @@ fun SearchScreen(
|
|||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
keyboard?.show()
|
keyboard?.show()
|
||||||
}
|
}
|
||||||
BackHandler(onBack = onBack)
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier.predictiveBack(onBack = onBack),
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -174,6 +173,7 @@ private fun SearchResults(
|
|||||||
SearchResultRow(
|
SearchResultRow(
|
||||||
event = event,
|
event = event,
|
||||||
position = positionOf(index, events.size),
|
position = positionOf(index, events.size),
|
||||||
|
modifier = calendarAnimateItem(),
|
||||||
onClick = { onEventClick(event) },
|
onClick = { onEventClick(event) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -184,10 +184,12 @@ private fun SearchResults(
|
|||||||
private fun SearchResultRow(
|
private fun SearchResultRow(
|
||||||
event: EventInstance,
|
event: EventInstance,
|
||||||
position: Position,
|
position: Position,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
|
modifier = modifier,
|
||||||
title = event.title,
|
title = event.title,
|
||||||
summary = searchSummary(event),
|
summary = searchSummary(event),
|
||||||
position = position,
|
position = position,
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ import de.jeanlucmakiola.calendula.qs.NewEventTileService
|
|||||||
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
|
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
|
||||||
import de.jeanlucmakiola.calendula.ui.crash.openIssueTracker
|
import de.jeanlucmakiola.calendula.ui.crash.openIssueTracker
|
||||||
import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport
|
import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold
|
import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold
|
||||||
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
||||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||||
@@ -674,7 +676,11 @@ private fun NotificationsScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = expanded) {
|
AnimatedVisibility(
|
||||||
|
visible = expanded,
|
||||||
|
enter = calendarExpandEnter(),
|
||||||
|
exit = calendarCollapseExit(),
|
||||||
|
) {
|
||||||
Column {
|
Column {
|
||||||
val timed = state.perCalendarReminderOverride.choiceFor(calendar.id)
|
val timed = state.perCalendarReminderOverride.choiceFor(calendar.id)
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberReduceMotion
|
||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
@@ -247,6 +249,8 @@ private fun WeekContent(
|
|||||||
val threshold = with(density) { 24.dp.toPx() }
|
val threshold = with(density) { 24.dp.toPx() }
|
||||||
var dragAccum by remember { mutableFloatStateOf(0f) }
|
var dragAccum by remember { mutableFloatStateOf(0f) }
|
||||||
val slideSpec = rememberCalendarSlideSpec()
|
val slideSpec = rememberCalendarSlideSpec()
|
||||||
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
|
val reduceMotion = rememberReduceMotion()
|
||||||
|
|
||||||
// Hoisted above the per-week AnimatedContent so the vertical scroll position
|
// Hoisted above the per-week AnimatedContent so the vertical scroll position
|
||||||
// survives week-to-week swipes (e.g. 18:00 stays centred). We only centre on
|
// survives week-to-week swipes (e.g. 18:00 stays centred). We only centre on
|
||||||
@@ -300,7 +304,7 @@ private fun WeekContent(
|
|||||||
WeekUiState.Loading -> "loading"
|
WeekUiState.Loading -> "loading"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transitionSpec = { calendarSlideTransition(slideDir, slideSpec) },
|
transitionSpec = { calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion) },
|
||||||
label = "week-transition",
|
label = "week-transition",
|
||||||
) { s ->
|
) { s ->
|
||||||
when (s) {
|
when (s) {
|
||||||
|
|||||||
Reference in New Issue
Block a user