Compact the selection bar instead of scrolling the event up (#141)
This commit is contained in:
@@ -588,7 +588,7 @@ fun CalendarHost(
|
||||
EventMoveHost(
|
||||
reschedule,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
raised = chromeHost.selectionBarShown,
|
||||
raised = chromeHost.selectionBarShown && !chromeHost.selectionCompact,
|
||||
)
|
||||
|
||||
// The bar a long-pressed event brings up in place of the FAB (#141).
|
||||
|
||||
@@ -82,6 +82,10 @@ class ChromeHost {
|
||||
var selectionBusy by mutableStateOf(false)
|
||||
internal set
|
||||
|
||||
/** Whether the bar would cover the selected event, and so draws compact. */
|
||||
var selectionCompact by mutableStateOf(false)
|
||||
internal set
|
||||
|
||||
/** Whether the selection's action bar is showing — it steps aside mid-gesture. */
|
||||
val selectionBarShown: Boolean get() = selection != null && !selectionBusy
|
||||
|
||||
@@ -108,7 +112,7 @@ fun PublishChrome(chrome: ScreenChrome, actions: ScreenActions) {
|
||||
|
||||
/** Hands the screen's selected [event] to the host, for as long as it stays selected. */
|
||||
@Composable
|
||||
fun PublishSelection(event: EventInstance?, busy: Boolean, onDismiss: () -> Unit) {
|
||||
fun PublishSelection(event: EventInstance?, busy: Boolean, compact: Boolean, onDismiss: () -> Unit) {
|
||||
val slot = LocalChromeSlot.current ?: return
|
||||
val currentDismiss by rememberUpdatedState(onDismiss)
|
||||
val selection = remember(event) { event?.let { EventSelection(it) { currentDismiss() } } }
|
||||
@@ -118,7 +122,12 @@ fun PublishSelection(event: EventInstance?, busy: Boolean, onDismiss: () -> Unit
|
||||
if (current) host.selection = selection
|
||||
onDispose { if (host.selection === selection) host.selection = null }
|
||||
}
|
||||
if (current) SideEffect { host.selectionBusy = busy && selection != null }
|
||||
if (current) {
|
||||
SideEffect {
|
||||
host.selectionBusy = busy && selection != null
|
||||
host.selectionCompact = compact && selection != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The top bar every calendar screen shares. */
|
||||
|
||||
@@ -11,6 +11,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreHoriz
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -18,6 +22,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -61,12 +66,16 @@ fun EventSelectionHost(
|
||||
// Held past the selection clearing, so the bar has something to draw on its way out.
|
||||
val held = remember { mutableStateOf<EventSelection?>(null) }
|
||||
host.selection?.let { held.value = it }
|
||||
// Over the selected event's bottom handle the bar shrinks to one button in
|
||||
// the FAB's corner; tapping it brings the whole bar back for this selection.
|
||||
var opened by remember(host.selection?.event, host.selectionCompact) { mutableStateOf(false) }
|
||||
val compact = host.selectionCompact && !opened
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
AnimatedVisibility(
|
||||
// Out of the way while the event is dragged or resized, so the
|
||||
// bottom of the timeline is there to drop on.
|
||||
visible = host.selectionBarShown,
|
||||
visible = host.selectionBarShown && !compact,
|
||||
enter = scaleIn(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeIn(),
|
||||
exit = scaleOut(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeOut(),
|
||||
modifier = Modifier
|
||||
@@ -106,6 +115,26 @@ fun EventSelectionHost(
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = host.selectionBarShown && compact,
|
||||
enter = scaleIn(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeIn(),
|
||||
exit = scaleOut(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeOut(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.navigationBarsPadding()
|
||||
.padding(end = SnackChipMargin, bottom = SnackChipMargin),
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = { opened = true },
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreHoriz,
|
||||
contentDescription = stringResource(R.string.event_selection_actions),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val message = when (failure) {
|
||||
null -> null
|
||||
EventActionFailure.LoadFailed -> stringResource(R.string.event_action_load_failed)
|
||||
@@ -129,7 +158,7 @@ fun EventSelectionHost(
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
start = SnackChipMargin,
|
||||
bottom = SnackChipMargin + if (host.selectionBarShown) SelectionBarClearance else 0.dp,
|
||||
bottom = SnackChipMargin + if (host.selectionBarShown && !compact) SelectionBarClearance else 0.dp,
|
||||
)
|
||||
.height(SnackChipHeight),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
@@ -137,7 +166,8 @@ fun EventSelectionHost(
|
||||
SnackChip(
|
||||
visible = failure != null,
|
||||
message = shownMessage.value,
|
||||
maxWidth = chipMaxWidth,
|
||||
// Clear of the compact button in the FAB's corner.
|
||||
maxWidth = chipMaxWidth - if (compact) SelectionBarClearance else 0.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.MutatePriority
|
||||
import androidx.compose.foundation.gestures.animateScrollBy
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -415,8 +414,13 @@ class TimelineDragController {
|
||||
|
||||
fun deselect() {
|
||||
selection = null
|
||||
selectionCovered = false
|
||||
}
|
||||
|
||||
/** Whether the selected block's bottom sits where the action bar would cover it. */
|
||||
var selectionCovered: Boolean by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
/**
|
||||
* Whether [block] is the selected event. Matched on the series and start too,
|
||||
* since the provider regenerates `Instances` rows and a re-read can bring the
|
||||
@@ -748,21 +752,17 @@ class TimelineDragController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the timeline so a just-selected block, [extraPx] of handle included,
|
||||
* clears the action bar [clearancePx] tall over the bottom of the viewport —
|
||||
* without pushing its top out of sight.
|
||||
* Note where the selected block's bottom edge, [extraPx] of handle included,
|
||||
* sits against the action bar [clearancePx] tall over the bottom of the
|
||||
* viewport. Under it, the bar draws itself compact out of the way (#141).
|
||||
*/
|
||||
suspend fun revealAboveBar(block: LayoutCoordinates, extraPx: Float, clearancePx: Float) {
|
||||
fun noteSelectedBottom(block: LayoutCoordinates, extraPx: Float, clearancePx: Float) {
|
||||
val viewport = geometry.viewport?.takeIf { it.isAttached } ?: return
|
||||
val scroll = geometry.scroll ?: return
|
||||
if (!block.isAttached) return
|
||||
val port = viewport.boundsInRoot()
|
||||
val top = block.positionInRoot().y
|
||||
val bottom = top + block.size.height + extraPx
|
||||
val needed = bottom - (port.bottom - clearancePx)
|
||||
val spare = top - port.top
|
||||
val by = minOf(needed, spare)
|
||||
if (by > 0f) scroll.animateScrollBy(by)
|
||||
val bottom = block.positionInRoot().y + block.size.height + extraPx
|
||||
val covered = bottom > port.bottom - clearancePx
|
||||
if (covered != selectionCovered) selectionCovered = covered
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,7 +67,6 @@ import androidx.compose.ui.geometry.Offset
|
||||
import de.jeanlucmakiola.calendula.ui.common.PickUp
|
||||
import de.jeanlucmakiola.calendula.ui.common.SelectionBarClearance
|
||||
import de.jeanlucmakiola.calendula.ui.common.bottomRoom
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import de.jeanlucmakiola.calendula.ui.common.ResizeEdge
|
||||
import de.jeanlucmakiola.calendula.ui.common.ResizeHandle
|
||||
import de.jeanlucmakiola.calendula.ui.common.ResizeHandleTarget
|
||||
@@ -571,6 +570,7 @@ private fun WeekContent(
|
||||
PublishSelection(
|
||||
event = selection?.event,
|
||||
busy = dragController.isDragging,
|
||||
compact = dragController.selectionCovered,
|
||||
onDismiss = dragController::deselect,
|
||||
)
|
||||
BackHandler(enabled = selection != null) { dragController.deselect() }
|
||||
@@ -1592,18 +1592,11 @@ private fun EventBlock(
|
||||
val lifted = draggable && dragController.ghosts(block)
|
||||
val ghost = ghostAlpha(lifted)
|
||||
val raise = selectionLift(selected)
|
||||
// Scrolled clear of the action bar as it is selected, handle and all.
|
||||
val blockCoordinates = remember { arrayOfNulls<LayoutCoordinates>(1) }
|
||||
// Where the selected event's bottom handle sits against the action bar, which
|
||||
// compacts itself rather than cover it. Read off the piece that ends the event.
|
||||
val revealExtraPx = with(density) { (ResizeHandleTarget / 2).toPx() }
|
||||
val barClearancePx = with(density) { SelectionBarClearance.toPx() }
|
||||
LaunchedEffect(selected) {
|
||||
if (!selected) return@LaunchedEffect
|
||||
// A frame for the room under the hours to be laid out first.
|
||||
withFrameNanos { }
|
||||
blockCoordinates[0]?.let {
|
||||
dragController.revealAboveBar(it, revealExtraPx, barClearancePx)
|
||||
}
|
||||
}
|
||||
val tracksBar = selected && !cuts.bottom
|
||||
// Not while the block is waiting on its own write: until then it still
|
||||
// shows the times from before it.
|
||||
LaunchedEffect(selected, block.event, lifted) {
|
||||
@@ -1620,7 +1613,15 @@ private fun EventBlock(
|
||||
// The source stays put as a ghost while its floating copy travels.
|
||||
.then(if (ghost < 1f) Modifier.alpha(ghost) else Modifier)
|
||||
.then(if (raise > 0f) Modifier.selectionLift(raise, shape) else Modifier)
|
||||
.onGloballyPositioned { blockCoordinates[0] = it },
|
||||
.then(
|
||||
if (tracksBar) {
|
||||
Modifier.onGloballyPositioned {
|
||||
dragController.noteSelectedBottom(it, revealExtraPx, barClearancePx)
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
<string name="event_move_done">Moved to %1$s</string>
|
||||
<string name="event_resize_done">Changed to %1$s</string>
|
||||
<string name="event_resize_recurring_title">Change recurring event</string>
|
||||
<string name="event_selection_actions">Event actions</string>
|
||||
<string name="event_action_load_failed">Couldn\'t open the event</string>
|
||||
<string name="event_move_undo">Undo</string>
|
||||
<string name="event_move_undone">Move undone</string>
|
||||
|
||||
Reference in New Issue
Block a user