diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourGutter.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourGutter.kt index afa19499..7dc762d9 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourGutter.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourGutter.kt @@ -19,6 +19,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity @@ -107,12 +109,25 @@ fun HourGutter( } } } - dragStartMin?.let { startMin -> + // Held while it fades, so it goes out where it was rather than blinking off. + val badgeMin = remember { mutableStateOf(null) } + dragStartMin?.let { badgeMin.value = it } + val badgeIn by animateFloatAsState( + targetValue = if (dragStartMin != null) 1f else 0f, + animationSpec = MaterialTheme.motionScheme.fastEffectsSpec(), + label = "gutterBadge", + ) + badgeMin.value?.takeIf { badgeIn > 0f }?.let { startMin -> val top = (hourHeight * (startMin / 60f) - BADGE_HEIGHT / 2).coerceAtLeast(0.dp) Box( modifier = Modifier .align(Alignment.TopCenter) .offset(y = top) + .graphicsLayer { + alpha = badgeIn + scaleX = 0.8f + 0.2f * badgeIn + scaleY = 0.8f + 0.2f * badgeIn + } .height(BADGE_HEIGHT) .background(MaterialTheme.colorScheme.primary, CircleShape) .padding(horizontal = 4.dp) 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 643925af..cb5a249d 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 @@ -1,7 +1,7 @@ package de.jeanlucmakiola.calendula.ui.common import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.snap +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.ScrollState import androidx.compose.foundation.MutatePriority @@ -117,7 +117,13 @@ data class TimelineDrag( * action bar up. [date] is the column the press landed in. [moved] holds from a * move or resize of it until the grid shows where it landed. */ -data class TimelineSelection(val event: EventInstance, val date: LocalDate, val moved: Boolean = false) +data class TimelineSelection( + val event: EventInstance, + val date: LocalDate, + val moved: Boolean = false, + /** Selected by letting go of a lifted block: the block takes the lift over as it is. */ + val fromLift: Boolean = false, +) /** * One day column's share of a dragged event, ready to draw. Both halves of an @@ -407,8 +413,8 @@ class TimelineDragController { var selection: TimelineSelection? by mutableStateOf(null) private set - fun select(event: EventInstance, date: LocalDate, moved: Boolean = false) { - selection = TimelineSelection(event, date, moved) + fun select(event: EventInstance, date: LocalDate, moved: Boolean = false, fromLift: Boolean = false) { + selection = TimelineSelection(event, date, moved, fromLift) } fun deselect() { @@ -1190,15 +1196,21 @@ private fun DragCopy( } } -/** How far a selected block is lifted, animated between 0 and 1 (#141). */ +/** + * How far a selected block is lifted, between 0 and 1 (#141). [snap] takes the + * lift over at once — from the floating copy it replaces — rather than raising + * the block a second time. + */ @Composable -fun selectionLift(selected: Boolean): Float { +fun selectionLift(selected: Boolean, snap: Boolean = false): Float { val reduceMotion = rememberReduceMotion() - return animateFloatAsState( - targetValue = if (selected) 1f else 0f, - animationSpec = if (reduceMotion) snap() else MaterialTheme.motionScheme.fastSpatialSpec(), - label = "selection-lift", - ).value + val spec = MaterialTheme.motionScheme.fastSpatialSpec() + val raise = remember { Animatable(if (selected) 1f else 0f) } + LaunchedEffect(selected) { + val target = if (selected) 1f else 0f + if (reduceMotion || (selected && snap)) raise.snapTo(target) else raise.animateTo(target, spec) + } + return raise.value } /** 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 5d8a1e71..8a3026f1 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 @@ -95,6 +95,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.ui.layout.layout import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.input.pointer.pointerInput @@ -1583,11 +1584,15 @@ private fun EventBlock( onMove = dragController::move, onDrop = { dragController.finish()?.let(onDrop) }, onCancel = dragController::cancel, - onRelease = { dragController.select(block.event, date) }, + onRelease = { dragController.select(block.event, date, fromLift = draggable) }, ) val lifted = draggable && dragController.ghosts(block) - val ghost = ghostAlpha(lifted) - val raise = selectionLift(selected) + // Let go of without moving, the block takes the floating copy's place as it + // stands: whole and lifted at once, not faded back up and raised again. + val takesOverLift = selected && dragController.selection?.fromLift == true + val ghostFade = ghostAlpha(lifted) + val ghost = if (takesOverLift && !lifted) 1f else ghostFade + val raise = selectionLift(selected, snap = takesOverLift) // 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() } @@ -1661,7 +1666,16 @@ private fun EventBlock( } } val end = { dragController.finishResize(zone)?.let(dragController.onResize) } - val handleAlpha = if (handlesShown) Modifier else Modifier.alpha(0f) + val handleIn by animateFloatAsState( + targetValue = if (handlesShown) 1f else 0f, + animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(), + label = "handles", + ) + val handleAlpha = Modifier.graphicsLayer { + alpha = handleIn.coerceIn(0f, 1f) + scaleX = 0.5f + 0.5f * handleIn + scaleY = 0.5f + 0.5f * handleIn + } if (!cuts.top) { ResizeHandle( color = accent,