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