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:
parent
f400bb357a
commit
acdb04f39d
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user