Revert the chip's FAB colours, quicken it, and end the settle flicker (#68)
The chip goes back to the neutral fill and its motion is cut to a receipt's length — the default visibility spring overshot and read as sluggish. The landed copy is now handed back when the grid actually draws the drop, not on a timer: releasing early put the source ghost back at full opacity in its old slot for the rest of the re-read, which is the flicker.
This commit is contained in:
@@ -6,8 +6,6 @@ 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.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -108,10 +106,6 @@ fun EventMoveHost(viewModel: RescheduleViewModel, modifier: Modifier = Modifier)
|
||||
actionLabel = stringResource(R.string.event_move_undo)
|
||||
.takeIf { shownUndo != null },
|
||||
onAction = shownUndo?.let { undo -> { viewModel.undo(undo) } },
|
||||
// The FAB's own colours: the chip shares its band, so sharing its
|
||||
// fill reads as one bottom layer rather than two competing ones.
|
||||
color = FloatingActionButtonDefaults.containerColor,
|
||||
contentColor = contentColorFor(FloatingActionButtonDefaults.containerColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,15 @@ class TimelineDragController {
|
||||
var isDragging: Boolean by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
/**
|
||||
* Whether the grid itself now draws the settled drop at its landing slot.
|
||||
* The copy may only be handed back once this is true: releasing on a timer
|
||||
* puts the source ghost back at full opacity in its *old* slot for whatever
|
||||
* is left of the re-read — a flicker of the event where it no longer is.
|
||||
*/
|
||||
var settledOnGrid: Boolean by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private var source: TimedBlock? = null
|
||||
private var grab = Offset.Zero
|
||||
private var pointer = Offset.Zero
|
||||
@@ -180,6 +189,25 @@ class TimelineDragController {
|
||||
originSlot = null
|
||||
drag = null
|
||||
settling = null
|
||||
settledOnGrid = false
|
||||
}
|
||||
|
||||
/**
|
||||
* What a day column now holds, so a settled drop can tell when the grid has
|
||||
* caught up with it. Matched on the landing slot plus either the event row
|
||||
* or its title: a single-occurrence move writes an exception row with a new
|
||||
* `eventId`, which nothing else about the drop can predict.
|
||||
*/
|
||||
fun noteGrid(date: LocalDate, blocks: List<TimedBlock>) {
|
||||
val landed = settling ?: return
|
||||
if (settledOnGrid || landed.date != date) return
|
||||
settledOnGrid = blocks.any { block ->
|
||||
block.startMin == landed.startMin &&
|
||||
(
|
||||
block.event.eventId == landed.event.eventId ||
|
||||
block.event.title == landed.event.title
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,6 +229,7 @@ class TimelineDragController {
|
||||
fun release() {
|
||||
settling = null
|
||||
liftedInstanceId = null
|
||||
settledOnGrid = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,11 +330,17 @@ fun TimelineDrop.startInstant(zone: TimeZone): Instant =
|
||||
LocalDateTime(date, LocalTime(startMin / 60, startMin % 60)).toInstant(zone)
|
||||
|
||||
/**
|
||||
* How long a landed block keeps its position after the write reports back — the
|
||||
* provider's change notification and the re-query land a beat later, and
|
||||
* releasing on the write alone would still flash the old slot.
|
||||
* A beat after the grid draws the drop, so its own block is under way before the
|
||||
* copy starts dissolving.
|
||||
*/
|
||||
const val SETTLE_GRACE_MILLIS: Long = 200L
|
||||
const val SETTLE_GRACE_MILLIS: Long = 60L
|
||||
|
||||
/**
|
||||
* How long to wait for a grid that never confirms the drop — the event landed on
|
||||
* a day this timeline doesn't show, or the write failed and nothing changed. The
|
||||
* copy has to go either way.
|
||||
*/
|
||||
private const val SETTLE_TIMEOUT_MILLIS = 900L
|
||||
|
||||
/**
|
||||
* The hand-over: the copy dissolves over this while the grid's own block slides
|
||||
@@ -337,12 +372,12 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier =
|
||||
if (controller.isDragging) controller.autoScroll()
|
||||
}
|
||||
// Hold the landed copy until the write is done — including the whole time a
|
||||
// recurring drop's scope dialog is up — then a grace for the re-read, then
|
||||
// fade it into the grid's own block, which has slid in underneath by now.
|
||||
// recurring drop's scope dialog is up — and then until the grid draws the
|
||||
// drop itself, so the copy dissolves onto a block that is already there.
|
||||
var handingOver by remember(controller.settling) { mutableStateOf(false) }
|
||||
LaunchedEffect(controller.settling, moveInFlight) {
|
||||
LaunchedEffect(controller.settling, moveInFlight, controller.settledOnGrid) {
|
||||
if (controller.settling == null || moveInFlight) return@LaunchedEffect
|
||||
delay(SETTLE_GRACE_MILLIS)
|
||||
delay(if (controller.settledOnGrid) SETTLE_GRACE_MILLIS else SETTLE_TIMEOUT_MILLIS)
|
||||
handingOver = true
|
||||
delay(SETTLE_FADE_MILLIS.toLong())
|
||||
controller.release()
|
||||
|
||||
@@ -615,6 +615,10 @@ private fun DayColumnCard(
|
||||
val hourPx = with(LocalDensity.current) { hourHeight.toPx() }
|
||||
val showHourLines = LocalShowHourLines.current
|
||||
val hourLineColor = MaterialTheme.colorScheme.outlineVariant
|
||||
// Tells a settled drop when this column has caught up with it.
|
||||
LaunchedEffect(blocks, dragController.settling) {
|
||||
dragController.noteGrid(date, blocks)
|
||||
}
|
||||
Card(
|
||||
// Plain rectangular column — the soft corners come from the outer
|
||||
// rounded scroll viewport, so inner rounding would look odd at the edges.
|
||||
|
||||
@@ -92,7 +92,6 @@ import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
||||
import de.jeanlucmakiola.calendula.ui.common.moveInFlight
|
||||
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
||||
import de.jeanlucmakiola.calendula.ui.common.DragSnapHaptics
|
||||
import de.jeanlucmakiola.calendula.ui.common.SETTLE_GRACE_MILLIS
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventMoveScope
|
||||
import de.jeanlucmakiola.calendula.domain.spanFirstDay
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
@@ -466,6 +465,9 @@ fun MonthScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/** How long a landed chip holds its day while the write and re-read land. */
|
||||
private const val MONTH_SETTLE_HOLD_MILLIS = 450L
|
||||
|
||||
/** The chip in flight, drawn over the grid and following the finger. */
|
||||
@Composable
|
||||
private fun MonthDragOverlay(controller: MonthDragController) {
|
||||
@@ -478,10 +480,12 @@ private fun MonthDragOverlay(controller: MonthDragController) {
|
||||
// every frame, and this composable is the one that is meant to.
|
||||
DragSnapHaptics(controller.drag?.targetDate)
|
||||
// Hold the landed chip until the write is done — including the whole time a
|
||||
// recurring drop's scope dialog is up — then a grace for the re-read.
|
||||
// recurring drop's scope dialog is up — then a grace for the re-read. A flat
|
||||
// wait, unlike the timeline's: the grid re-lays a whole month of rows, so
|
||||
// there is no single column to watch for the chip arriving.
|
||||
LaunchedEffect(controller.settling, moveInFlight) {
|
||||
if (controller.settling == null || moveInFlight) return@LaunchedEffect
|
||||
delay(SETTLE_GRACE_MILLIS)
|
||||
delay(MONTH_SETTLE_HOLD_MILLIS)
|
||||
controller.release()
|
||||
}
|
||||
Box(
|
||||
|
||||
@@ -764,6 +764,10 @@ private fun DayColumnCard(
|
||||
val hourPx = with(LocalDensity.current) { hourHeight.toPx() }
|
||||
val showHourLines = LocalShowHourLines.current
|
||||
val hourLineColor = MaterialTheme.colorScheme.outlineVariant
|
||||
// Tells a settled drop when this column has caught up with it.
|
||||
LaunchedEffect(blocks, dragController.settling) {
|
||||
dragController.noteGrid(date, blocks)
|
||||
}
|
||||
Card(
|
||||
// Plain rectangular columns — the soft corners come from the outer
|
||||
// rounded scroll viewport, so inner rounding would look odd at the edges.
|
||||
|
||||
Submodule floret-kit updated: 0ce855c925...fd86bc4a24
Reference in New Issue
Block a user