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.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -108,10 +106,6 @@ fun EventMoveHost(viewModel: RescheduleViewModel, modifier: Modifier = Modifier)
|
|||||||
actionLabel = stringResource(R.string.event_move_undo)
|
actionLabel = stringResource(R.string.event_move_undo)
|
||||||
.takeIf { shownUndo != null },
|
.takeIf { shownUndo != null },
|
||||||
onAction = shownUndo?.let { undo -> { viewModel.undo(undo) } },
|
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)
|
var isDragging: Boolean by mutableStateOf(false)
|
||||||
private set
|
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 source: TimedBlock? = null
|
||||||
private var grab = Offset.Zero
|
private var grab = Offset.Zero
|
||||||
private var pointer = Offset.Zero
|
private var pointer = Offset.Zero
|
||||||
@@ -180,6 +189,25 @@ class TimelineDragController {
|
|||||||
originSlot = null
|
originSlot = null
|
||||||
drag = null
|
drag = null
|
||||||
settling = 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() {
|
fun release() {
|
||||||
settling = null
|
settling = null
|
||||||
liftedInstanceId = null
|
liftedInstanceId = null
|
||||||
|
settledOnGrid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -301,11 +330,17 @@ fun TimelineDrop.startInstant(zone: TimeZone): Instant =
|
|||||||
LocalDateTime(date, LocalTime(startMin / 60, startMin % 60)).toInstant(zone)
|
LocalDateTime(date, LocalTime(startMin / 60, startMin % 60)).toInstant(zone)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long a landed block keeps its position after the write reports back — the
|
* A beat after the grid draws the drop, so its own block is under way before the
|
||||||
* provider's change notification and the re-query land a beat later, and
|
* copy starts dissolving.
|
||||||
* releasing on the write alone would still flash the old slot.
|
|
||||||
*/
|
*/
|
||||||
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
|
* 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()
|
if (controller.isDragging) controller.autoScroll()
|
||||||
}
|
}
|
||||||
// Hold the landed copy until the write is done — including the whole time a
|
// 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
|
// recurring drop's scope dialog is up — and then until the grid draws the
|
||||||
// fade it into the grid's own block, which has slid in underneath by now.
|
// drop itself, so the copy dissolves onto a block that is already there.
|
||||||
var handingOver by remember(controller.settling) { mutableStateOf(false) }
|
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
|
if (controller.settling == null || moveInFlight) return@LaunchedEffect
|
||||||
delay(SETTLE_GRACE_MILLIS)
|
delay(if (controller.settledOnGrid) SETTLE_GRACE_MILLIS else SETTLE_TIMEOUT_MILLIS)
|
||||||
handingOver = true
|
handingOver = true
|
||||||
delay(SETTLE_FADE_MILLIS.toLong())
|
delay(SETTLE_FADE_MILLIS.toLong())
|
||||||
controller.release()
|
controller.release()
|
||||||
|
|||||||
@@ -615,6 +615,10 @@ private fun DayColumnCard(
|
|||||||
val hourPx = with(LocalDensity.current) { hourHeight.toPx() }
|
val hourPx = with(LocalDensity.current) { hourHeight.toPx() }
|
||||||
val showHourLines = LocalShowHourLines.current
|
val showHourLines = LocalShowHourLines.current
|
||||||
val hourLineColor = MaterialTheme.colorScheme.outlineVariant
|
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(
|
Card(
|
||||||
// Plain rectangular column — the soft corners come from the outer
|
// Plain rectangular column — the soft corners come from the outer
|
||||||
// rounded scroll viewport, so inner rounding would look odd at the edges.
|
// 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.moveInFlight
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.DragSnapHaptics
|
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.ui.common.EventMoveScope
|
||||||
import de.jeanlucmakiola.calendula.domain.spanFirstDay
|
import de.jeanlucmakiola.calendula.domain.spanFirstDay
|
||||||
import androidx.compose.ui.unit.IntSize
|
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. */
|
/** The chip in flight, drawn over the grid and following the finger. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun MonthDragOverlay(controller: MonthDragController) {
|
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.
|
// every frame, and this composable is the one that is meant to.
|
||||||
DragSnapHaptics(controller.drag?.targetDate)
|
DragSnapHaptics(controller.drag?.targetDate)
|
||||||
// Hold the landed chip until the write is done — including the whole time a
|
// 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) {
|
LaunchedEffect(controller.settling, moveInFlight) {
|
||||||
if (controller.settling == null || moveInFlight) return@LaunchedEffect
|
if (controller.settling == null || moveInFlight) return@LaunchedEffect
|
||||||
delay(SETTLE_GRACE_MILLIS)
|
delay(MONTH_SETTLE_HOLD_MILLIS)
|
||||||
controller.release()
|
controller.release()
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -764,6 +764,10 @@ private fun DayColumnCard(
|
|||||||
val hourPx = with(LocalDensity.current) { hourHeight.toPx() }
|
val hourPx = with(LocalDensity.current) { hourHeight.toPx() }
|
||||||
val showHourLines = LocalShowHourLines.current
|
val showHourLines = LocalShowHourLines.current
|
||||||
val hourLineColor = MaterialTheme.colorScheme.outlineVariant
|
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(
|
Card(
|
||||||
// Plain rectangular columns — the soft corners come from the outer
|
// Plain rectangular columns — the soft corners come from the outer
|
||||||
// rounded scroll viewport, so inner rounding would look odd at the edges.
|
// 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