Stop the moved event flickering once it lands (#68)
Two causes, both leaving the block at the old slot or dimmed for a frame or several. The source is now identified for the write's duration by event row plus the slot it left, not by instance id — the provider renumbers Instances rows, so a re-read still carrying the old time arrives under a new id and the ghost stopped matching. And the ghost is dropped when the copy *starts* dissolving rather than after: un-ghosting last left the block dim under the fading copy and brightened it once the copy was gone.
This commit is contained in:
@@ -140,6 +140,16 @@ class TimelineDragController {
|
||||
var liftedInstanceId: Long? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
/**
|
||||
* Where the settled drop came from, and which event row it belongs to. The
|
||||
* instance id alone can't identify the source block for the length of the
|
||||
* write: the provider regenerates `Instances` rows, so a re-read that still
|
||||
* carries the *old* time can arrive under a new instance id — and a source
|
||||
* matched by instance id would stop being the ghost and flash back to full
|
||||
* opacity in the slot the event is about to leave.
|
||||
*/
|
||||
private var settledOrigin: Triple<Long, LocalDate, Int>? by mutableStateOf(null)
|
||||
|
||||
/** Whether a finger is on a block right now — [settling] is not dragging. */
|
||||
var isDragging: Boolean by mutableStateOf(false)
|
||||
private set
|
||||
@@ -190,6 +200,22 @@ class TimelineDragController {
|
||||
drag = null
|
||||
settling = null
|
||||
settledOnGrid = false
|
||||
settledOrigin = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether [block] is the one whose copy is in flight, and so must stay a
|
||||
* ghost. While the finger holds it that is the instance it picked up; once
|
||||
* dropped it is also whatever now sits in the slot it left, however the
|
||||
* provider has renumbered it in the meantime.
|
||||
*/
|
||||
fun ghosts(block: TimedBlock, date: LocalDate): Boolean {
|
||||
if (liftedInstanceId == null) return false
|
||||
if (block.event.instanceId == liftedInstanceId) return true
|
||||
val (eventId, originDate, originMin) = settledOrigin ?: return false
|
||||
return block.event.eventId == eventId &&
|
||||
date == originDate &&
|
||||
block.startMin == originMin
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,14 +248,26 @@ class TimelineDragController {
|
||||
if (landed == null || landed.slot == origin) return null
|
||||
settling = landed
|
||||
liftedInstanceId = landed.event.instanceId
|
||||
settledOrigin = origin?.let { (date, min) -> Triple(landed.event.eventId, date, min) }
|
||||
return TimelineDrop(landed.event, landed.date, landed.startMin)
|
||||
}
|
||||
|
||||
/** Hand a settled drop back to the grid, which by now draws it itself. */
|
||||
/**
|
||||
* Stop ghosting the source — the grid draws the drop itself by now — while
|
||||
* the copy is still on screen dissolving. Un-ghosting only at [release]
|
||||
* would leave the block under the fading copy dim, and brighten it once the
|
||||
* copy was gone: a dip the eye reads as the event flickering.
|
||||
*/
|
||||
fun handOver() {
|
||||
liftedInstanceId = null
|
||||
settledOrigin = null
|
||||
}
|
||||
|
||||
/** Drop the copy, once it has faded into the grid's own block. */
|
||||
fun release() {
|
||||
settling = null
|
||||
liftedInstanceId = null
|
||||
settledOnGrid = false
|
||||
handOver()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -379,6 +417,7 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier =
|
||||
if (controller.settling == null || moveInFlight) return@LaunchedEffect
|
||||
delay(if (controller.settledOnGrid) SETTLE_GRACE_MILLIS else SETTLE_TIMEOUT_MILLIS)
|
||||
handingOver = true
|
||||
controller.handOver()
|
||||
delay(SETTLE_FADE_MILLIS.toLong())
|
||||
controller.release()
|
||||
}
|
||||
|
||||
@@ -733,7 +733,7 @@ private fun EventBlock(
|
||||
onDrop = { dragController.finish()?.let(onDrop) },
|
||||
onCancel = dragController::cancel,
|
||||
)
|
||||
val lifted = draggable && dragController.liftedInstanceId == block.event.instanceId
|
||||
val lifted = draggable && dragController.ghosts(block, date)
|
||||
val ghost = ghostAlpha(lifted)
|
||||
Box(
|
||||
modifier = modifier
|
||||
|
||||
@@ -147,10 +147,19 @@ class MonthDragController {
|
||||
return MonthChipDrop(landed.event, landed.grabDate, target)
|
||||
}
|
||||
|
||||
/** Hand a settled drop back to the grid, which by now draws it itself. */
|
||||
/**
|
||||
* Stop ghosting the source while the copy is still dissolving — un-ghosting
|
||||
* only at [release] leaves the chip under it dim and then brightens it, a
|
||||
* dip the eye reads as the event flickering.
|
||||
*/
|
||||
fun handOver() {
|
||||
liftedInstanceId = null
|
||||
}
|
||||
|
||||
/** Drop the copy, once it has faded into the grid's own chip. */
|
||||
fun release() {
|
||||
settling = null
|
||||
liftedInstanceId = null
|
||||
handOver()
|
||||
}
|
||||
|
||||
/** Root x of [date]'s column, in whichever visible row shows that day. */
|
||||
|
||||
@@ -92,6 +92,7 @@ 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_FADE_MILLIS
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventMoveScope
|
||||
import de.jeanlucmakiola.calendula.domain.spanFirstDay
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
@@ -483,9 +484,13 @@ private fun MonthDragOverlay(controller: MonthDragController) {
|
||||
// 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.
|
||||
var handingOver by remember(controller.settling) { mutableStateOf(false) }
|
||||
LaunchedEffect(controller.settling, moveInFlight) {
|
||||
if (controller.settling == null || moveInFlight) return@LaunchedEffect
|
||||
delay(MONTH_SETTLE_HOLD_MILLIS)
|
||||
handingOver = true
|
||||
controller.handOver()
|
||||
delay(SETTLE_FADE_MILLIS.toLong())
|
||||
controller.release()
|
||||
}
|
||||
Box(
|
||||
@@ -503,6 +508,11 @@ private fun MonthDragOverlay(controller: MonthDragController) {
|
||||
targetValue = if (controller.drag == null || reduceMotion) 0f else 1f,
|
||||
label = "chip-lift",
|
||||
)
|
||||
val copyAlpha by animateFloatAsState(
|
||||
targetValue = if (handingOver) 0f else 1f,
|
||||
animationSpec = tween(SETTLE_FADE_MILLIS),
|
||||
label = "chip-handover",
|
||||
)
|
||||
MonthBar(
|
||||
event = drag.event,
|
||||
dark = dark,
|
||||
@@ -524,6 +534,7 @@ private fun MonthDragOverlay(controller: MonthDragController) {
|
||||
scaleX = 1f + 0.04f * lift
|
||||
scaleY = 1f + 0.04f * lift
|
||||
shadowElevation = 8.dp.toPx() * lift
|
||||
alpha = copyAlpha
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
clip = false
|
||||
},
|
||||
|
||||
@@ -902,7 +902,7 @@ private fun EventBlock(
|
||||
onDrop = { dragController.finish()?.let(onDrop) },
|
||||
onCancel = dragController::cancel,
|
||||
)
|
||||
val lifted = draggable && dragController.liftedInstanceId == block.event.instanceId
|
||||
val lifted = draggable && dragController.ghosts(block, date)
|
||||
val ghost = ghostAlpha(lifted)
|
||||
Box(
|
||||
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
||||
|
||||
Reference in New Issue
Block a user