Ease a released long press into the selection (#141)
This commit is contained in:
@@ -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<Int?>(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)
|
||||
|
||||
@@ -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<Float>()
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user