1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-09-18 21:12:24 +02:00

Support multi target dragging

This commit is contained in:
Allan Wang 2023-06-21 18:08:38 -07:00
parent e653e77249
commit f400bb357a
No known key found for this signature in database
GPG Key ID: C93E3F9C679D7A56
5 changed files with 222 additions and 148 deletions

View File

@ -1,138 +0,0 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.pitchedapps.frost.compose
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.compositionLocalOf
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.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import kotlin.math.roundToInt
/*
* Resources:
*
* https://blog.canopas.com/android-drag-and-drop-ui-element-in-jetpack-compose-14922073b3f1
*/
@Composable
fun DragContainer(modifier: Modifier = Modifier, content: @Composable DragScope.() -> Unit) {
val draggable = remember { Draggable() }
val dragScope = remember(draggable) { DragScopeImpl(draggable) }
Box(modifier = modifier) {
dragScope.content()
DraggingContent(draggable = draggable)
}
}
private class DragScopeImpl(private val draggable: Draggable) : DragScope {
@Composable
override fun DragTarget(
key: String,
content: @Composable BoxScope.(isDragging: Boolean) -> Unit
) {
var isDragging by remember { mutableStateOf(false) }
var positionInWindow by remember { mutableStateOf(Offset.Zero) }
var size by remember { mutableStateOf(IntSize.Zero) }
Box(
modifier =
Modifier.onGloballyPositioned {
positionInWindow = it.positionInWindow()
size = it.size
}
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
isDragging = true
draggable.composable = content
draggable.composableSize = size
draggable.dragPosition = positionInWindow
},
onDrag = { _, offset -> draggable.dragPosition += offset },
onDragEnd = {
isDragging = false
draggable.composable = null
},
)
},
) {
if (!isDragging) {
content(false)
}
}
}
}
private fun IntSize.toDpSize(density: Density): DpSize {
return with(density) { DpSize(width.toDp(), height.toDp()) }
}
@Composable
private fun DraggingContent(draggable: Draggable) {
val composable = draggable.composable ?: return
val density = LocalDensity.current
val sizeDp =
remember { derivedStateOf { draggable.composableSize?.toDpSize(density) } }.value ?: return
Box(
modifier = Modifier.size(sizeDp).offset { draggable.dragPosition.toIntOffset() },
) {
composable(true)
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
@LayoutScopeMarker
@Immutable
interface DragScope {
@Composable
fun DragTarget(key: String, content: @Composable BoxScope.(isDragging: Boolean) -> Unit)
}
class Draggable {
var composable by mutableStateOf<(@Composable BoxScope.(isDragging: Boolean) -> Unit)?>(null)
var composableSize by mutableStateOf<IntSize?>(null)
var dragPosition by mutableStateOf(Offset.Zero)
}
internal val LocalDraggable = compositionLocalOf {}

View File

@ -0,0 +1,148 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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.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.input.pointer.pointerInput
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
/*
* Resources:
*
* https://blog.canopas.com/android-drag-and-drop-ui-element-in-jetpack-compose-14922073b3f1
*/
@Composable
fun DragContainer(
modifier: Modifier = Modifier,
draggableState: DraggableState,
content: @Composable () -> Unit
) {
Box(modifier = modifier) {
content()
DraggingContents(draggableState = draggableState)
}
}
/**
* Drag target.
*
* The [content] composable may be composed where [DragTarget] is defined, or in [DraggingContents]
* 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.
*/
@Composable
fun DragTarget(
key: String? = null,
draggableState: DraggableState,
content: @Composable BoxScope.(isDragging: Boolean) -> Unit
) {
var isDragging by remember { mutableStateOf(false) }
var positionInWindow by remember { mutableStateOf(Offset.Zero) }
var size by remember { mutableStateOf(IntSize.Zero) }
Box(
modifier =
Modifier.onGloballyPositioned {
positionInWindow = it.positionInWindow()
size = it.size
}
.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,
)
},
onDrag = { _, offset ->
if (!isDragging) return@detectDragGesturesAfterLongPress
val target = draggableState.targets[key] ?: return@detectDragGesturesAfterLongPress
target.dragPosition += offset
},
onDragEnd = {
if (isDragging) {
draggableState.targets.remove(key)
}
isDragging = false
},
)
},
) {
if (!isDragging) {
content(false)
}
}
}
/**
* Draggable content.
*
* Provides composition for all dragging targets.
*
* For composing, we take the position provided by the dragging target. We also account for the
* target size, as we want to maintain the same bounds here. As an example, the target can have
* fillMaxWidth in a grid, but would have a full parent width here without the sizing constraints.
*/
@Composable
private fun DraggingContents(draggableState: DraggableState) {
for (target in draggableState.targets.values) {
DraggingContent(target = target)
}
}
@Composable
private fun DraggingContent(target: DraggingTargetState) {
val density = LocalDensity.current
Box(
modifier =
Modifier.size(target.size.toDpSize(density)).offset { target.dragPosition.toIntOffset() },
) {
target.composable(this, true)
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2023 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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.unit.IntSize
@Composable
fun rememberDraggableState(): DraggableState {
return remember { DraggableState() }
}
@Stable
class DraggableState {
val targets = mutableStateMapOf<String?, DraggingTargetState>()
}
/** State for individual dragging target. */
class DraggingTargetState(
val composable: @Composable BoxScope.(isDragging: Boolean) -> Unit,
val size: IntSize,
dragPosition: Offset
) {
var dragPosition by mutableStateOf(dragPosition)
}

View File

@ -16,7 +16,21 @@
*/
package com.pitchedapps.frost.ext
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import kotlin.math.roundToInt
fun Modifier.thenIf(condition: Boolean, action: () -> Modifier): Modifier =
@Composable
fun Modifier.thenIf(condition: Boolean, action: @Composable () -> Modifier): Modifier =
if (condition) then(action()) else this
fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
fun IntSize.toDpSize(density: Density): DpSize {
return with(density) { DpSize(width.toDp(), height.toDp()) }
}

View File

@ -43,7 +43,9 @@ 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.DragContainer
import com.pitchedapps.frost.compose.draggable.DragContainer
import com.pitchedapps.frost.compose.draggable.DragTarget
import com.pitchedapps.frost.compose.draggable.rememberDraggableState
import com.pitchedapps.frost.compose.effects.rememberShakeState
import com.pitchedapps.frost.compose.effects.shake
import com.pitchedapps.frost.ext.thenIf
@ -81,24 +83,25 @@ fun TabSelector(
unselected: List<TabData>,
onSelect: (List<TabData>) -> Unit
) {
DragContainer(modifier = modifier) {
val draggableState = rememberDraggableState()
DragContainer(modifier = modifier, draggableState = draggableState) {
Column(modifier = Modifier.statusBarsPadding()) {
LazyVerticalGrid(
modifier = Modifier.weight(1f),
columns = GridCells.Fixed(4),
) {
items(unselected, key = { it.key }) {
this@DragContainer.DragTarget(key = it.key) { isDragging ->
val shakeState = rememberShakeState()
DragTarget(key = it.key, draggableState = draggableState) { isDragging ->
TabItem(
modifier =
Modifier.thenIf(!isDragging) { Modifier.animateItemPlacement() }
.shake(shakeState)
.clickable {
Modifier.thenIf(!isDragging) {
val shakeState = rememberShakeState()
Modifier.animateItemPlacement().shake(shakeState).clickable {
shakeState.shake()
// onSelect(listOf(it))
},
}
},
data = it,
)
}