Last active
February 3, 2022 00:15
-
-
Save jugyo/3d8249100fc3f2ab34ada0495e117bcc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How it looks.