Skip to content

Instantly share code, notes, and snippets.

@jugyo
Last active February 3, 2022 00:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jugyo/3d8249100fc3f2ab34ada0495e117bcc to your computer and use it in GitHub Desktop.
Save jugyo/3d8249100fc3f2ab34ada0495e117bcc to your computer and use it in GitHub Desktop.
package com.example.compose_drag_and_drop
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.compose_drag_and_drop.ui.theme.Compose_drag_and_dropTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var items by remember {
mutableStateOf(listOf("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"))
}
Compose_drag_and_dropTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
DraggableGrid(
modifier = Modifier.padding(16.dp),
column = 4,
onDrag = { from, to ->
items = items.mapIndexed { index, item ->
when {
index == to -> items[from]
// *, *, *, [to], *, [from], *, *, *
to < from -> {
if (index in to..from) {
items[index - 1]
} else {
item
}
}
// *, *, *, *, *, [from], *, [to], *, *
to > from -> {
if (index in from..to) {
items[index + 1]
} else {
item
}
}
else -> item
}
}
}
) {
itemsIndexed(items) { index, item ->
Box(
modifier = Modifier
.padding(4.dp)
.aspectRatio(1f)
.clip(RoundedCornerShape(8.dp))
.background(Color.Blue.copy(alpha = 0.3f))
) {
Text(
text = item,
modifier = Modifier.align(alignment = Alignment.Center),
fontSize = 24.sp
)
}
}
}
}
}
}
}
}
@Composable
fun DraggableGrid(
column: Int,
modifier: Modifier = Modifier,
onDrag: (from: Int, to: Int) -> Unit,
content: DraggableGridScope.() -> Unit
) {
val scope = DraggableGridScope()
scope.apply(content)
var logicalItemPlaces by remember { mutableStateOf<List<DraggableGridItemLayoutInfo>>(emptyList()) }
var draggedItemIndex by remember { mutableStateOf<Int?>(null) }
var hoveredItemIndex by remember { mutableStateOf<Int?>(null) }
var draggedDistance by remember { mutableStateOf(Offset.Zero) }
fun resetDragState() {
draggedItemIndex = null
hoveredItemIndex = null
draggedDistance = Offset.Zero
}
Layout(
modifier = modifier
.fillMaxWidth()
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
draggedItemIndex =
logicalItemPlaces.firstOrNull { it.contains(offset) }?.index
},
onDrag = { change, dragAmount ->
change.consumeAllChanges()
draggedDistance += dragAmount
hoveredItemIndex =
logicalItemPlaces
.firstOrNull { it.contains(change.position) }
?.let { if (it.index == draggedItemIndex) null else it.index }
},
onDragEnd = {
if (draggedItemIndex != null && hoveredItemIndex != null) {
onDrag(draggedItemIndex!!, hoveredItemIndex!!)
}
resetDragState()
},
onDragCancel = ::resetDragState
)
},
content = {
repeat(scope.totalSize) { index ->
Box(
modifier = Modifier
.graphicsLayer {
if (index == draggedItemIndex) {
translationX = draggedDistance.x
translationY = draggedDistance.y - 4.dp.roundToPx()
alpha = 0.9f
}
}
) {
scope.contentFor(index).invoke()
}
}
}
) { measurables, constraints ->
val width = constraints.maxWidth / column
val height = width
val placeables =
measurables.map {
it.measure(Constraints.fixed(width = width, height = height))
}
layout(width = constraints.maxWidth, height = constraints.minHeight) {
val logicalPlaces = mutableListOf<DraggableGridItemLayoutInfo>()
fun placeFor(index: Int): IntOffset {
val rowIndex = index / column
val columnIndex = index % column
val x = width * columnIndex
val y = rowIndex * height
return IntOffset(x = x, y = y)
}
placeables.forEachIndexed { index, placeable ->
val originalPlace = placeFor(index)
logicalPlaces += DraggableGridItemLayoutInfo(
index = index,
x = originalPlace.x,
y = originalPlace.y,
width = width,
height = height,
)
val visiblePlace =
if (draggedItemIndex != null && hoveredItemIndex != null) {
when {
index == draggedItemIndex -> originalPlace
// *, *, *, [hovered], *, [dragged], *, *, *
index < draggedItemIndex!! &&
index >= hoveredItemIndex!! -> placeFor(index + 1)
// *, *, *, *, *, [dragged], *, [hovered], *, *
index > draggedItemIndex!! &&
index <= hoveredItemIndex!! -> placeFor(index - 1)
else -> originalPlace
}
} else {
originalPlace
}
placeable.place(
x = visiblePlace.x,
y = visiblePlace.y,
zIndex = if (index == draggedItemIndex) 1f else 0f
)
}
logicalItemPlaces = logicalPlaces.toList()
}
}
}
private data class DraggableGridItemLayoutInfo(
val index: Int,
val x: Int,
val y: Int,
val width: Int,
val height: Int,
)
private fun DraggableGridItemLayoutInfo.contains(position: Offset) =
position.y.toInt() in y..(y + height) &&
position.x.toInt() in x..(x + width)
class DraggableGridScope {
private val interval = ArrayList<(Int) -> (@Composable () -> Unit)>()
val totalSize get() = interval.size
fun contentFor(index: Int): @Composable () -> Unit {
val interval = interval[index]
return interval(index)
}
fun <T> items(items: List<T>, itemContent: @Composable (item: T) -> Unit) {
items.forEach { item ->
interval.add {
@Composable { itemContent(item) }
}
}
}
fun <T> itemsIndexed(items: List<T>, itemContent: @Composable (index: Int, item: T) -> Unit) {
items.forEachIndexed { index, item ->
interval.add {
@Composable { itemContent(index, item) }
}
}
}
}
@jugyo
Copy link
Author

jugyo commented Feb 2, 2022

How it looks.

screen-20220202-183139

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment