From 5567957475e6e142c08a5e3064bdb4f50f903436 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 21 Jun 2023 22:31:09 -0700 Subject: [PATCH] Create modifiers for dragging --- .../frost/compose/draggable/DragContainer.kt | 121 +++++++----------- .../frost/compose/draggable/DraggableState.kt | 87 ++++++++----- .../frost/tabselector/TabSelectorScreen.kt | 9 +- 3 files changed, 103 insertions(+), 114 deletions(-) diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DragContainer.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DragContainer.kt index 36627f293..0663660bb 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DragContainer.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DragContainer.kt @@ -18,24 +18,16 @@ package com.pitchedapps.frost.compose.draggable import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.draw.alpha import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.IntSize import com.pitchedapps.frost.ext.toDpSize import com.pitchedapps.frost.ext.toIntOffset @@ -72,80 +64,55 @@ fun DragContainer( fun DragTarget( key: String = "", draggableState: DraggableState, - content: @Composable BoxScope.(isDragging: Boolean) -> Unit + content: @Composable (isDragging: Boolean) -> Unit ) { - var isDragging by remember { mutableStateOf(false) } - - var positionInWindow by remember { mutableStateOf(Offset.Zero) } - - var size by remember { mutableStateOf(IntSize.Zero) } + val dragTargetState = + draggableState.rememberDragTarget( + key = key, + content = content, + ) Box( - modifier = - Modifier.onGloballyPositioned { - positionInWindow = it.positionInWindow() - size = it.size - } - .pointerInput(Unit) { - detectDragGesturesAfterLongPress( - onDragStart = { - isDragging = - draggableState.onDragStart(key) { - DraggingTargetState( - composable = content, - size = size, - dragPosition = positionInWindow, - ) - } - }, - onDrag = { _, offset -> - if (isDragging) { - draggableState.onDrag(key, offset) - } - }, - onDragEnd = { - if (isDragging) { - draggableState.onDragEnd(key) - isDragging = false - } - }, - ) - }, + modifier = Modifier.dragTarget(dragTargetState), ) { - if (!isDragging) { - content(false) + content(false) + } +} + +private fun Modifier.dragTarget(dragTargetState: DragTargetState): Modifier { + return onGloballyPositioned { + dragTargetState.windowPosition = it.positionInWindow() + dragTargetState.size = it.size } - } + .pointerInput(dragTargetState) { + val draggableState = dragTargetState.draggableState + val key = dragTargetState.key + + detectDragGesturesAfterLongPress( + onDragStart = { + dragTargetState.dragPosition = dragTargetState.windowPosition + dragTargetState.isDragging = draggableState.onDragStart(key, dragTargetState) + }, + onDrag = { _, offset -> + if (dragTargetState.isDragging) { + draggableState.onDrag(key, offset) + } + }, + onDragEnd = { + if (dragTargetState.isDragging) { + draggableState.onDragEnd(key) + dragTargetState.isDragging = false + } + }, + ) + } + // We still need to draw to track size changes + .alpha(if (dragTargetState.isDragging) 0f else 1f) } -fun Modifier.dropTarget(key: String, draggableState: DraggableState): Modifier { - return onGloballyPositioned { draggableState.onDropUpdateBounds(key, it.boundsInWindow()) } -} - -@Composable -fun DropTarget( - draggableState: DraggableState, - onDrop: (T) -> Unit, - content: @Composable BoxScope.(isHovering: Boolean, data: T?) -> Unit -) { - var hoverKey: String? by remember { mutableStateOf(null) } - - var hoverData: T? by remember { mutableStateOf(null) } - - var bounds: Rect by remember { mutableStateOf(Rect.Zero) } - - LaunchedEffect(hoverKey, bounds, draggableState) { - // hoverKey = draggableState.getHoverKey(hoverKey, bounds) - - // println("asdf new hover key $hoverKey") - } - - Box( - modifier = Modifier.onGloballyPositioned { bounds = it.boundsInWindow() }, - ) { - content(hoverKey != null, hoverData) - } +fun Modifier.dropTarget(dropTargetState: DropTargetState): Modifier { + return onGloballyPositioned { dropTargetState.bounds = it.boundsInWindow() } } /** @@ -165,12 +132,12 @@ private fun DraggingContents(draggableState: DraggableState) { } @Composable -private fun DraggingContent(target: DraggingTargetState) { +private fun DraggingContent(target: DragTargetState) { val density = LocalDensity.current Box( modifier = Modifier.size(target.size.toDpSize(density)).offset { target.dragPosition.toIntOffset() }, ) { - target.composable(this, true) + target.composable(true) } } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DraggableState.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DraggableState.kt index 5236e9ee1..19808e74d 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DraggableState.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DraggableState.kt @@ -16,8 +16,8 @@ */ package com.pitchedapps.frost.compose.draggable -import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf @@ -34,7 +34,7 @@ fun rememberDraggableState(): DraggableState { interface DraggableState { - val targets: Collection + val targets: Collection /** * Being drag for target [key]. @@ -44,39 +44,43 @@ interface DraggableState { * Returns true if the request is accepted. It is the caller's responsibility to not propagate * drag events if the request is denied. */ - fun onDragStart(key: String, targetState: () -> DraggingTargetState): Boolean + fun onDragStart(key: String, dragTargetState: DragTargetState): Boolean fun onDrag(key: String, offset: Offset) fun onDragEnd(key: String) - fun onDropUpdateBounds(key: String, bounds: Rect) + @Composable + fun rememberDragTarget( + key: String, + content: @Composable (isDragging: Boolean) -> Unit + ): DragTargetState - fun dropTarget(key: String): DropTargetState? + @Composable fun rememberDropTarget(key: String): DropTargetState } class DraggableStateImpl : DraggableState { - private val dragTargets = mutableStateMapOf() + private val activeDragTargets = mutableStateMapOf() private val dropTargets = mutableStateMapOf() - override val targets: Collection - get() = dragTargets.values + override val targets: Collection + get() = activeDragTargets.values - override fun onDragStart(key: String, targetState: () -> DraggingTargetState): Boolean { - if (key in dragTargets) return false - dragTargets[key] = targetState() + override fun onDragStart(key: String, dragTargetState: DragTargetState): Boolean { + if (key in activeDragTargets) return false + activeDragTargets[key] = dragTargetState return true } override fun onDrag(key: String, offset: Offset) { - val position = dragTargets[key] ?: return + val position = activeDragTargets[key] ?: return position.dragPosition += offset checkForDrag(key) } override fun onDragEnd(key: String) { - dragTargets.remove(key) + activeDragTargets.remove(key) for ((dropKey, dropTarget) in dropTargets) { if (dropTarget.hoverKey == key) { setHover(dragKey = null, dropKey) @@ -86,42 +90,56 @@ class DraggableStateImpl : DraggableState { } } - override fun onDropUpdateBounds(key: String, bounds: Rect) { - dropTargets.getOrPut(key) { DropTargetState() }.bounds = bounds - checkForDrop(key) + @Composable + override fun rememberDragTarget( + key: String, + content: @Composable (isDragging: Boolean) -> Unit + ): DragTargetState { + val target = + remember(key, content, this) { + DragTargetState(key = key, draggableState = this, composable = content) + } + DisposableEffect(target) { onDispose { activeDragTargets.remove(key) } } + return target } - override fun dropTarget(key: String): DropTargetState? { - return dropTargets[key] + @Composable + override fun rememberDropTarget(key: String): DropTargetState { + val target = remember(key, this) { DropTargetState(key, this) } + DisposableEffect(target) { + dropTargets[key] = target + + onDispose { dropTargets.remove(key) } + } + return target } private fun setHover(dragKey: String?, dropKey: String) { val dropTarget = dropTargets[dropKey] ?: return dropTarget.hoverKey = dragKey - println("asdf update $dragKey $dropKey") } /** Returns true if drag target exists and is within bounds */ private fun DropTargetState.hasValidDragTarget(): Boolean { val currentKey = hoverKey ?: return false // no target - val dragTarget = dragTargets[currentKey] ?: return false // target not valid + val dragTarget = activeDragTargets[currentKey] ?: return false // target not valid return dragTarget.within(bounds) } /** Check if drag target fits in drop */ - private fun checkForDrop(dropKey: String) { + internal fun checkForDrop(dropKey: String) { val dropTarget = dropTargets[dropKey] ?: return val bounds = dropTarget.bounds if (dropTarget.hasValidDragTarget()) return // Find first target that matches - val dragKey = dragTargets.entries.firstOrNull { it.value.within(bounds) }?.key + val dragKey = activeDragTargets.entries.firstOrNull { it.value.within(bounds) }?.key setHover(dragKey = dragKey, dropKey = dropKey) } /** Check drops for drag target fit */ - private fun checkForDrag(dragKey: String) { - val dragTarget = dragTargets[dragKey] ?: return + internal fun checkForDrag(dragKey: String) { + val dragTarget = activeDragTargets[dragKey] ?: return for ((dropKey, dropTarget) in dropTargets) { // Do not override targets that are valid if (dropTarget.hasValidDragTarget()) continue @@ -134,23 +152,30 @@ class DraggableStateImpl : DraggableState { } } -private fun DraggingTargetState?.within(bounds: Rect): Boolean { +private fun DragTargetState?.within(bounds: Rect): Boolean { if (this == null) return false val center = dragPosition + Offset(size.width * 0.5f, size.height * 0.5f) return bounds.contains(center) } /** State for individual dragging target. */ -class DraggingTargetState( - val composable: @Composable BoxScope.(isDragging: Boolean) -> Unit, - val size: IntSize, - dragPosition: Offset +class DragTargetState( + val key: String, + val draggableState: DraggableStateImpl, + val composable: @Composable (isDragging: Boolean) -> Unit ) { - var dragPosition by mutableStateOf(dragPosition) + var isDragging by mutableStateOf(false) + var windowPosition = Offset.Zero + var dragPosition by mutableStateOf(Offset.Zero) + var size: IntSize by mutableStateOf(IntSize.Zero) } -class DropTargetState { +class DropTargetState(private val key: String, private val draggableState: DraggableStateImpl) { var hoverKey: String? by mutableStateOf(null) var hoverData: String? by mutableStateOf(null) var bounds: Rect = Rect.Zero + set(value) { + field = value + draggableState.checkForDrop(key) + } } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt index 60a1d4b84..41ee7e095 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -130,18 +129,16 @@ fun TabBottomBar( ) { NavigationBar(modifier = modifier) { items.forEach { item -> - val key = item.key - - val hasHoverKey by derivedStateOf { draggableState.dropTarget(key)?.hoverKey != null } + val dropTargetState = draggableState.rememberDropTarget(item.key) val alpha by animateFloatAsState( - targetValue = if (!hasHoverKey) 1f else 0f, + targetValue = if (dropTargetState.hoverKey == null) 1f else 0f, label = "Nav Item Alpha", ) NavigationBarItem( - modifier = Modifier.dropTarget(key, draggableState), + modifier = Modifier.dropTarget(dropTargetState), icon = { // println(dropTargetState.hoverKey) Icon(