diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthMorph.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthMorph.kt index 1cd42c9..7dcf4c0 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthMorph.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthMorph.kt @@ -72,6 +72,29 @@ internal fun Modifier.morphElement(key: MonthMorphKey): Modifier { } } +/** + * Lift *untagged* content into the shared-transition overlay for the duration of + * a transition, so the travelling pieces don't bury it. + * + * Tagged content is drawn in the overlay, which is painted above the whole + * regular tree. Anything left in that tree therefore disappears behind it while a + * transition runs, however it was layered locally — which is what hid the split + * style's selection outline behind its own cell pill from the first frame of an + * expand to the last. + * + * The content keeps its own layout bounds; only where it is *painted* changes. So + * the outline still draws at the size the collapsed cell actually is, which is + * the whole reason it was left untagged. + */ +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +internal fun Modifier.morphOverlay(zIndex: Float = 1f): Modifier { + val morph = LocalMonthMorph.current ?: return this + return with(morph.shared) { + this@morphOverlay.renderInSharedTransitionScopeOverlay(zIndexInOverlay = zIndex) + } +} + /** * Tag content that means the same thing but *is drawn differently* on each side — * a 5dp dot and a titled bar — so only the bounds are shared and the contents 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 c504759..5585973 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 @@ -1052,31 +1052,35 @@ private fun SplitMonthCollapsed( val reduceMotion = rememberReduceMotion() Column(Modifier.fillMaxSize()) { - AnimatedContent( - // The selection travels *with* the state so each page keeps its - // own. Read from outside, both pages would show the incoming - // one, and paging visibly threw the marker across the outgoing - // grid — onto the new month's 1st, which the old grid still - // shows among its trailing days — before the new page arrived. - targetState = state to selected, - modifier = swipeModifier, - // Keyed on the month alone, so a provider notification refreshing - // the month you are on — or a tap moving the selection within it - // — updates in place instead of sliding. - contentKey = { (s, _) -> s.month }, - transitionSpec = { - calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion) - }, - label = "split-month-transition", - ) { (s, sel) -> - SplitMonthGrid( - state = s, - selected = sel, - showWeekNumbers = showWeekNumbers, - onSelectDay = onSelectDay, - ) + // Grid and handle drag as one surface — the handle is what advertises the + // gesture, so it has to answer to it as well as to a tap. The pane is + // outside: it scrolls, and is full of tappable rows. + Column(swipeModifier) { + AnimatedContent( + // The selection travels *with* the state so each page keeps its + // own. Read from outside, both pages would show the incoming + // one, and paging visibly threw the marker across the outgoing + // grid — onto the new month's 1st, which the old grid still + // shows among its trailing days — before the new page arrived. + targetState = state to selected, + // Keyed on the month alone, so a provider notification refreshing + // the month you are on — or a tap moving the selection within it + // — updates in place instead of sliding. + contentKey = { (s, _) -> s.month }, + transitionSpec = { + calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion) + }, + label = "split-month-transition", + ) { (s, sel) -> + SplitMonthGrid( + state = s, + selected = sel, + showWeekNumbers = showWeekNumbers, + onSelectDay = onSelectDay, + ) + } + SplitExpandHandle(expanded = false, onToggle = onExpand) } - SplitExpandHandle(expanded = false, onToggle = onExpand) SplitDayPane( date = selected, today = state.today, @@ -1115,10 +1119,12 @@ private fun SplitMonthExpanded( val fadeSpec = rememberCalendarFadeSpec() val reduceMotion = rememberReduceMotion() - Column(Modifier.fillMaxSize()) { + // The whole screen drags here, handle included — there is no pane to keep out + // of it, and the handle is the obvious thing to reach for on the way back. + Column(Modifier.fillMaxSize().then(swipeModifier)) { AnimatedContent( targetState = state, - modifier = Modifier.weight(1f).then(swipeModifier), + modifier = Modifier.weight(1f), contentKey = { it.month }, transitionSpec = { calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion) @@ -1340,9 +1346,15 @@ private fun SplitDayCell( // flashing over a grid that no longer had full-height cells. Untagged it // is laid out where the collapsed cell actually is and simply fades in, // which is the only size it is ever true at. + // + // Untagged is not enough on its own, though: the tagged pieces paint into + // an overlay above the whole regular tree, so the outline spent every + // transition hidden behind its own pill and reappeared when it ended. + // morphOverlay lifts it into that same overlay while keeping its bounds. Box( Modifier .fillMaxSize() + .morphOverlay() .drawBehind { val alpha = selection.value if (alpha <= 0f) return@drawBehind