Jetpack Compose, the modern UI toolkit for Android, offers a declarative way to build user interfaces. One common UI interaction is drag-and-drop, allowing users to move items around the screen. Implementing drag-and-drop in Jetpack Compose involves tracking touch events, managing state, and updating the UI accordingly.
What is Drag-and-Drop?
Drag-and-drop is a user interface interaction where a user can click on an item, drag it to a new location, and release it to drop the item in its new position. This interaction is common in applications like Kanban boards, photo editors, and list reordering tools.
Why Use Drag-and-Drop?
- Enhanced User Experience: Provides intuitive interaction for reordering or moving items.
- Increased Engagement: Allows users to manipulate and organize content directly.
- Improved Productivity: Facilitates efficient content management.
How to Implement Drag-and-Drop Gestures in Jetpack Compose
To implement drag-and-drop in Jetpack Compose, you’ll need to manage touch events using modifiers like pointerInput
, maintain the state of the dragged item, and update the UI based on drag events.
Step 1: Add Dependencies
Ensure you have the necessary dependencies in your build.gradle
file:
dependencies {
implementation("androidx.compose.ui:ui:1.6.1")
implementation("androidx.compose.foundation:foundation:1.6.1")
implementation("androidx.compose.material:material:1.6.1")
implementation("androidx.compose.runtime:runtime:1.6.1")
implementation("androidx.compose.ui:ui-tooling-preview:1.6.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.1")
}
Step 2: Create a Draggable Composable
First, create a composable that can be dragged. Use the pointerInput
modifier to detect touch events and update the position of the composable accordingly.
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
@Composable
fun DraggableItem(
data: String,
onDragStarted: (String) -> Unit,
onDragEnded: () -> Unit,
modifier: Modifier = Modifier
) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Card(
modifier = modifier
.offset(offsetX.dp, offsetY.dp)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
onDragStarted(data)
},
onDrag = { change, dragAmount ->
change.consume()
offsetX += dragAmount.x
offsetY += dragAmount.y
},
onDragEnd = {
onDragEnded()
offsetX = 0f
offsetY = 0f
},
onDragCancel = {
offsetX = 0f
offsetY = 0f
}
)
}
) {
Box(
modifier = Modifier.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(text = data)
}
}
}
Explanation:
offsetX
andoffsetY
: These state variables track the current offset of the item being dragged.pointerInput
: Used to detect drag gestures.detectDragGestures
: A coroutine that simplifies handling drag events:onDragStart
: Called when the drag starts.onDrag
: Called repeatedly as the item is dragged. It updates theoffsetX
andoffsetY
based on the drag amount.onDragEnd
: Called when the drag ends. Resets theoffsetX
andoffsetY
to 0.onDragCancel
: Called if the drag is cancelled (e.g., touch is interrupted). Resets theoffsetX
andoffsetY
to 0.
offset
Modifier: Applies the current offset to the composable, moving it on the screen.
Step 3: Create a Drop Target Composable
Create a composable that acts as a drop target. This target will highlight when a draggable item is over it and handle the drop action.
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
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.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
@Composable
fun DropTarget(
modifier: Modifier = Modifier,
onDataDropped: (String) -> Unit,
content: @Composable (isHovering: Boolean) -> Unit
) {
var isHovering by remember { mutableStateOf(false) }
var targetSize by remember { mutableStateOf(IntSize.Zero) }
Box(
modifier = modifier
.onGloballyPositioned { coordinates ->
targetSize = coordinates.size
},
contentAlignment = Alignment.Center
) {
val isDroppingHere = LocalDragTargetInfo.current.dragPosition in VisibleBounds(targetSize)
if (isDroppingHere) {
isHovering = true
}
content(isHovering)
LaunchedEffect(isDroppingHere) {
if (isDroppingHere) {
onDataDropped(LocalDragTargetInfo.current.draggedData)
LocalDragTargetInfo.current.dataToDrop = null
LocalDragTargetInfo.current.dragPosition = Offset.Zero
LocalDragTargetInfo.current.isDragging = false
isHovering = false // Reset hovering state after dropping
}
}
}
}
//Extension function to verify if the Dragged item's coordinates overlaps with any of the drop targets available on the screen.
//Note - Please verify that both of these targets are placed on same layout
private fun VisibleBounds(targetSize: IntSize): ClosedRange {
val rect = Rect(Offset.Zero, Offset(targetSize.width.toFloat(), targetSize.height.toFloat()))
return rect.topLeft..rect.bottomRight
}
// Data class mimicking android.graphics.Rect
data class Rect(val topLeft: Offset, val bottomRight: Offset) {
init {
require(topLeft.x <= bottomRight.x) { "Left must be <= right" }
require(topLeft.y <= bottomRight.y) { "Top must be <= bottom" }
}
fun width() = bottomRight.x - topLeft.x
fun height() = bottomRight.y - topLeft.y
}
Explanation:
- LocalDragTargetInfo- To globally access the dragged item coordinates.
- isDroppingHere- It determines whether to render UI in drag in progress or normal rendering by accessing the item coordinates to match if there are overlappings of these target points of all other Composables laid out on screen to create drop point on it.
- Drop Effect: A basic extension fun checks if the location of drag matches current composables location and it has to accept or revert back to normal screen
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.geometry.Offset
data class DragTargetInfo(
var isDragging: Boolean = false,
var dragPosition: Offset = Offset.Zero,
var draggedData: String = "",
var dataToDrop:String?=null
)
val LocalDragTargetInfo = staticCompositionLocalOf {
DragTargetInfo()
}
Step 4: Implement the Drag-and-Drop Logic in the Main Composable
Now, integrate the DraggableItem
and DropTarget
composables in a main composable to create a drag-and-drop interaction.
import androidx.compose.foundation.layout.*
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.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
@Composable
fun MainScreen() {
var draggedItem by remember { mutableStateOf("") }
var items by remember { mutableStateOf(listOf("Item 1", "Item 2", "Item 3")) }
var dropTarget1Data by remember { mutableStateOf("Drop Target 1") }
var dropTarget2Data by remember { mutableStateOf("Drop Target 2") }
var dropTarget3Data by remember { mutableStateOf("Drop Target 3") }
//Provide Local value as we do for Theme Color
CompositionLocalProvider(
LocalDragTargetInfo provides dragTargetInfo
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(text = "Drag and Drop Example", style = MaterialTheme.typography.h6)
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
DropTarget(
modifier = Modifier
.width(150.dp)
.height(100.dp),
onDataDropped = { data ->
dropTarget1Data = "Dropped: $data"
}
) { isHovering ->
Surface(color = if (isHovering) Color.Green else Color.Gray) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = dropTarget1Data, color = Color.White)
}
}
}
DropTarget(
modifier = Modifier
.width(150.dp)
.height(100.dp),
onDataDropped = { data ->
dropTarget2Data = "Dropped: $data"
}
) { isHovering ->
Surface(color = if (isHovering) Color.Green else Color.Gray) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = dropTarget2Data, color = Color.White)
}
}
}
}
Column (modifier = Modifier.fillMaxWidth(),horizontalAlignment = Alignment.CenterHorizontally){
Text("Drag item to below targets to swap items ", style = MaterialTheme.typography.subtitle1, modifier = Modifier.padding(5.dp))
//Drop target for drag Item here
DropTarget(
modifier = Modifier
.fillMaxWidth()
.height(70.dp)
.padding(3.dp),
onDataDropped = { data ->
dropTarget3Data = "Swapped item = $data"
}
) { isHovering ->
Surface(color = if (isHovering) Color.Green else Color.Blue) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = dropTarget3Data, color = Color.White)
}
}
}
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
items.forEach { item ->
DraggableItem(
data = item,
onDragStarted = { data ->
draggedItem = data
dragTargetInfo.isDragging = true
dragTargetInfo.draggedData = data
Log.d("DragAndDrop", "Draggable Start ${dragTargetInfo.isDragging} ")
},
onDragEnded = {
draggedItem = ""
dragTargetInfo.isDragging = false
Log.d("DragAndDrop", "Draggable End ${dragTargetInfo.isDragging}")
}
)
}
}
}
}
}
Conclusion
Implementing drag-and-drop gestures in Jetpack Compose involves using touch event handling with modifiers like pointerInput
and managing state to track the position of dragged items. By combining draggable composables and drop targets, you can create intuitive and interactive UI experiences in your Android applications. Understanding these techniques will allow you to build complex and engaging user interfaces that leverage the power of Jetpack Compose.