diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt index feeca09..f4686b8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt @@ -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? 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() } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt index 2016224..3eae341 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt @@ -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 diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthDrag.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthDrag.kt index ea64659..43698fe 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthDrag.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthDrag.kt @@ -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. */ diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index 948ae6c..b812f29 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -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 }, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt index a5da0e4..4cf6392 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt @@ -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)