1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 12:02:33 +01:00

Manually link events for better bound computation

This commit is contained in:
Allan Wang 2023-06-21 21:00:37 -07:00
parent f400bb357a
commit acdb04f39d
No known key found for this signature in database
GPG Key ID: C93E3F9C679D7A56
3 changed files with 191 additions and 29 deletions

View File

@ -22,13 +22,16 @@ 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.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
@ -62,11 +65,12 @@ fun DragContainer(
* depending on drag state. Keep this in mind based on the isDragging flag.
*
* [key] is used to distinguish between multiple dragging targets. If only one should be used at a
* time, this can be nullable. If there is a key conflict, only the first target will be dragged.
* time, this can be the same key. If there is a key conflict, only the first target will be
* dragged.
*/
@Composable
fun DragTarget(
key: String? = null,
key: String = "",
draggableState: DraggableState,
content: @Composable BoxScope.(isDragging: Boolean) -> Unit
) {
@ -86,30 +90,25 @@ fun DragTarget(
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
if (draggableState.targets.containsKey(key)) {
// We are already dragging an item with the same key, ignore
isDragging = false
return@detectDragGesturesAfterLongPress
}
isDragging = true
draggableState.targets[key] =
DraggingTargetState(
composable = content,
size = size,
dragPosition = positionInWindow,
)
isDragging =
draggableState.onDragStart(key) {
DraggingTargetState(
composable = content,
size = size,
dragPosition = positionInWindow,
)
}
},
onDrag = { _, offset ->
if (!isDragging) return@detectDragGesturesAfterLongPress
val target = draggableState.targets[key] ?: return@detectDragGesturesAfterLongPress
target.dragPosition += offset
if (isDragging) {
draggableState.onDrag(key, offset)
}
},
onDragEnd = {
if (isDragging) {
draggableState.targets.remove(key)
draggableState.onDragEnd(key)
isDragging = false
}
isDragging = false
},
)
},
@ -120,6 +119,35 @@ fun DragTarget(
}
}
fun Modifier.dropTarget(key: String, draggableState: DraggableState): Modifier {
return onGloballyPositioned { draggableState.onDropUpdateBounds(key, it.boundsInWindow()) }
}
@Composable
fun <T> 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)
}
}
/**
* Draggable content.
*
@ -131,7 +159,7 @@ fun DragTarget(
*/
@Composable
private fun DraggingContents(draggableState: DraggableState) {
for (target in draggableState.targets.values) {
for (target in draggableState.targets) {
DraggingContent(target = target)
}
}

View File

@ -18,23 +18,126 @@ package com.pitchedapps.frost.compose.draggable
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.IntSize
@Composable
fun rememberDraggableState(): DraggableState {
return remember { DraggableState() }
return remember { DraggableStateImpl() }
}
@Stable
class DraggableState {
val targets = mutableStateMapOf<String?, DraggingTargetState>()
interface DraggableState {
val targets: Collection<DraggingTargetState>
/**
* Being drag for target [key].
*
* If there is another target with the same key being dragged, we will ignore this request.
*
* 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 onDrag(key: String, offset: Offset)
fun onDragEnd(key: String)
fun onDropUpdateBounds(key: String, bounds: Rect)
fun dropTarget(key: String): DropTargetState?
}
class DraggableStateImpl : DraggableState {
private val dragTargets = mutableStateMapOf<String, DraggingTargetState>()
private val dropTargets = mutableStateMapOf<String, DropTargetState>()
override val targets: Collection<DraggingTargetState>
get() = dragTargets.values
override fun onDragStart(key: String, targetState: () -> DraggingTargetState): Boolean {
if (key in dragTargets) return false
dragTargets[key] = targetState()
return true
}
override fun onDrag(key: String, offset: Offset) {
val position = dragTargets[key] ?: return
position.dragPosition += offset
checkForDrag(key)
}
override fun onDragEnd(key: String) {
dragTargets.remove(key)
for ((dropKey, dropTarget) in dropTargets) {
if (dropTarget.hoverKey == key) {
setHover(dragKey = null, dropKey)
// Check other drag targets in case one meets drag requirements
checkForDrop(dropKey)
}
}
}
override fun onDropUpdateBounds(key: String, bounds: Rect) {
dropTargets.getOrPut(key) { DropTargetState() }.bounds = bounds
checkForDrop(key)
}
override fun dropTarget(key: String): DropTargetState? {
return dropTargets[key]
}
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
return dragTarget.within(bounds)
}
/** Check if drag target fits in drop */
private 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
setHover(dragKey = dragKey, dropKey = dropKey)
}
/** Check drops for drag target fit */
private fun checkForDrag(dragKey: String) {
val dragTarget = dragTargets[dragKey] ?: return
for ((dropKey, dropTarget) in dropTargets) {
// Do not override targets that are valid
if (dropTarget.hasValidDragTarget()) continue
if (dragTarget.within(dropTarget.bounds)) {
setHover(dragKey = dragKey, dropKey = dropKey)
} else if (dropTarget.hoverKey == dragKey) {
setHover(dragKey = null, dropKey = dropKey)
}
}
}
}
private fun DraggingTargetState?.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. */
@ -45,3 +148,9 @@ class DraggingTargetState(
) {
var dragPosition by mutableStateOf(dragPosition)
}
class DropTargetState {
var hoverKey: String? by mutableStateOf(null)
var hoverData: String? by mutableStateOf(null)
var bounds: Rect = Rect.Zero
}

View File

@ -16,6 +16,7 @@
*/
package com.pitchedapps.frost.tabselector
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -33,18 +34,22 @@ 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
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pitchedapps.frost.compose.draggable.DragContainer
import com.pitchedapps.frost.compose.draggable.DragTarget
import com.pitchedapps.frost.compose.draggable.DraggableState
import com.pitchedapps.frost.compose.draggable.dropTarget
import com.pitchedapps.frost.compose.draggable.rememberDraggableState
import com.pitchedapps.frost.compose.effects.rememberShakeState
import com.pitchedapps.frost.compose.effects.shake
@ -108,19 +113,39 @@ fun TabSelector(
}
}
TabBottomBar(modifier = Modifier.navigationBarsPadding(), items = selected)
TabBottomBar(
modifier = Modifier.navigationBarsPadding(),
draggableState = draggableState,
items = selected,
)
}
}
}
@Composable
fun TabBottomBar(modifier: Modifier = Modifier, items: List<TabData>) {
fun TabBottomBar(
modifier: Modifier = Modifier,
draggableState: DraggableState,
items: List<TabData>
) {
NavigationBar(modifier = modifier) {
items.forEach { item ->
val key = item.key
val hasHoverKey by derivedStateOf { draggableState.dropTarget(key)?.hoverKey != null }
val alpha by
animateFloatAsState(
targetValue = if (!hasHoverKey) 1f else 0f,
label = "Nav Item Alpha",
)
NavigationBarItem(
modifier = Modifier.dropTarget(key, draggableState),
icon = {
// println(dropTargetState.hoverKey)
Icon(
modifier = Modifier.size(24.dp),
modifier = Modifier.size(24.dp).alpha(alpha),
imageVector = item.icon,
contentDescription = item.title,
)