fix(month): drag from the handle, and stop the outline hiding behind its cell (#53)
The handle advertised a gesture it didn't answer to — the drag lived on the grid alone, so the one part of the screen that says "pull me" was the one part that ignored being pulled. Grid and handle now drag as a single surface; the pane stays outside it, since it scrolls and is full of tappable rows. Tapping the handle still toggles: a parent drag and a child clickable coexist the same way they do in any scrollable list. The selection outline was still flickering through an expand or collapse, and being untagged was not enough to explain it. Tagged pieces paint into an overlay above the whole regular tree, so the outline — correctly left in that tree, to keep the collapsed cell's bounds — spent every transition hidden behind its own morphing pill and reappeared the moment it ended. It now renders into that same overlay while keeping its own bounds, which is what the untagged layout was for in the first place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 —
|
* 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
|
* a 5dp dot and a titled bar — so only the bounds are shared and the contents
|
||||||
|
|||||||
@@ -1052,31 +1052,35 @@ private fun SplitMonthCollapsed(
|
|||||||
val reduceMotion = rememberReduceMotion()
|
val reduceMotion = rememberReduceMotion()
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize()) {
|
Column(Modifier.fillMaxSize()) {
|
||||||
AnimatedContent(
|
// Grid and handle drag as one surface — the handle is what advertises the
|
||||||
// The selection travels *with* the state so each page keeps its
|
// gesture, so it has to answer to it as well as to a tap. The pane is
|
||||||
// own. Read from outside, both pages would show the incoming
|
// outside: it scrolls, and is full of tappable rows.
|
||||||
// one, and paging visibly threw the marker across the outgoing
|
Column(swipeModifier) {
|
||||||
// grid — onto the new month's 1st, which the old grid still
|
AnimatedContent(
|
||||||
// shows among its trailing days — before the new page arrived.
|
// The selection travels *with* the state so each page keeps its
|
||||||
targetState = state to selected,
|
// own. Read from outside, both pages would show the incoming
|
||||||
modifier = swipeModifier,
|
// one, and paging visibly threw the marker across the outgoing
|
||||||
// Keyed on the month alone, so a provider notification refreshing
|
// grid — onto the new month's 1st, which the old grid still
|
||||||
// the month you are on — or a tap moving the selection within it
|
// shows among its trailing days — before the new page arrived.
|
||||||
// — updates in place instead of sliding.
|
targetState = state to selected,
|
||||||
contentKey = { (s, _) -> s.month },
|
// Keyed on the month alone, so a provider notification refreshing
|
||||||
transitionSpec = {
|
// the month you are on — or a tap moving the selection within it
|
||||||
calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion)
|
// — updates in place instead of sliding.
|
||||||
},
|
contentKey = { (s, _) -> s.month },
|
||||||
label = "split-month-transition",
|
transitionSpec = {
|
||||||
) { (s, sel) ->
|
calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion)
|
||||||
SplitMonthGrid(
|
},
|
||||||
state = s,
|
label = "split-month-transition",
|
||||||
selected = sel,
|
) { (s, sel) ->
|
||||||
showWeekNumbers = showWeekNumbers,
|
SplitMonthGrid(
|
||||||
onSelectDay = onSelectDay,
|
state = s,
|
||||||
)
|
selected = sel,
|
||||||
|
showWeekNumbers = showWeekNumbers,
|
||||||
|
onSelectDay = onSelectDay,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SplitExpandHandle(expanded = false, onToggle = onExpand)
|
||||||
}
|
}
|
||||||
SplitExpandHandle(expanded = false, onToggle = onExpand)
|
|
||||||
SplitDayPane(
|
SplitDayPane(
|
||||||
date = selected,
|
date = selected,
|
||||||
today = state.today,
|
today = state.today,
|
||||||
@@ -1115,10 +1119,12 @@ private fun SplitMonthExpanded(
|
|||||||
val fadeSpec = rememberCalendarFadeSpec()
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
val reduceMotion = rememberReduceMotion()
|
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(
|
AnimatedContent(
|
||||||
targetState = state,
|
targetState = state,
|
||||||
modifier = Modifier.weight(1f).then(swipeModifier),
|
modifier = Modifier.weight(1f),
|
||||||
contentKey = { it.month },
|
contentKey = { it.month },
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion)
|
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
|
// 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,
|
// is laid out where the collapsed cell actually is and simply fades in,
|
||||||
// which is the only size it is ever true at.
|
// 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(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.morphOverlay()
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
val alpha = selection.value
|
val alpha = selection.value
|
||||||
if (alpha <= 0f) return@drawBehind
|
if (alpha <= 0f) return@drawBehind
|
||||||
|
|||||||
Reference in New Issue
Block a user