Drag and Drop in Jetpack Compose
Introduction
In January 2024, a major update to Jetpack Compose added two new modifiers: dragAndDropSource
And dragAndDropTarget
. In this article I will tell you how to implement the Drag and Drop effect in Jetpack Compose.
Composable function for displaying food card
First, let's write the code for the food card.
fun FoodItemCard(foodItem: FoodItem) {
Card(
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 10.dp),
colors = CardDefaults.elevatedCardColors(
containerColor = Color.White,
), shape = RoundedCornerShape(24.dp),
modifier = Modifier.padding(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(10.dp)
) {
Image(
painter = painterResource(id = foodItem.image),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(130.dp)
.clip(RoundedCornerShape(16.dp))
)
Spacer(modifier = Modifier.width(20.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = foodItem.name,
fontSize = 22.sp,
color = Color.DarkGray
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "$${foodItem.price}",
fontSize = 18.sp,
color = Color.Black,
fontWeight = FontWeight.ExtraBold
)
}
}
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 10.dp)
) {
items(items = foodList) { food ->
FoodItemCard(foodItem = food)
}
}
Composable function for displaying a person's card
Next, we will write the code for the person’s card.
@Composable
fun PersonCard(person: Person) {
Column(
modifier = Modifier
.padding(6.dp)
.shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp))
.width(width = 120.dp)
.fillMaxHeight(0.8f)
.background(Color.White, RoundedCornerShape(16.dp)),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = person.profile), contentDescription = null,
modifier = Modifier
.size(70.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = person.name,
fontSize = 18.sp,
color = Color.Black,
fontWeight = FontWeight.Bold
)
}
}
LazyRow(
modifier = Modifier
.fillMaxHeight(0.3f)
.fillMaxWidth()
.background(Color.LightGray, shape = RoundedCornerShape(topEnd = 10.dp, topStart = 10.dp))
.padding(vertical = 10.dp)
.align(Alignment.BottomCenter),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
items(items = persons) { person ->
PersonCard(person)
}
}
Let's add a source for Drag and Drop
Before we get started, a little background on modifiers:
Modifier.dragAndDropSource
This modifier allows the component to become the source of Drag and Drop.
@Composable
fun MyDraggableComponent() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.dragAndDropSource(
drawDragDecoration = {
// UI перетаскиваемого элемента
}
) {
// логика обработки Drag and Drop
startTransfer (/* данные для передачи */ )
}
) { /* контент перетаскиваемого компонента */ }
}
Modifier.dragAndDropSource
takes two parameters:drawDragDecoration
Andblock
;drawDragDecoration
is a lambda block that provides a visual representation of the component that is being dragged by Drag and Drop;block
this is a lambda block, it acceptsDragAndDropSourceScope
as a receiver. It allows you to detect a Drag and Drop gesture and subsequently you can process it;call
startTransfer
in the lambda initializes the Drag and Drop action.
Now let's add this modifier to the food card:
Here, transferData
defines the content that should be transferred during Drag and Drop. For example, transferring data from a food card to a person’s card.
private const val foodItemTransferAction = "action_foodItem"
private const val foodItemTransferData = "data_foofdItem"
...
startTransfer(
DragAndDropTransferData(
clipData = ClipData.newIntent(
"foodItem",
Intent(foodItemTransferAction).apply {
putExtra(
foodItemTransferData,
Gson().toJson(foodItem)
)
},
)
)
The food card is now draggable.
Let's add a place for Drop and Drop
Second help on modifiers:
Modifier.dragAndDropTarget
This modifier in Jetpack Compose allows composable functions to receive Drop and Drop events.
@Composable
fun MyDragTarget() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green)
.dragAndDropTarget(
shouldStartDragAndDrop = { startEvent-> return true },
target = object : DragAndDropTarget { ... }
)
) { /* контент перетаскиваемого компонента */ }
}
It takes two parameters:
shouldStartDragAndDrop
: tells the component whether to receive drag events fromDragAndDropEvent
.target
—DragAndDropTarget
receives the following events from Drag and Drop:onDrop(event)
: This function will be called when a component is dragged insideDragAndDropTarget
. Meaningtrue
indicates that the event is fromDragAndDropEvent
was processed, on the contraryfalse
indicates that it has been rejected;onStarted(event)
: This function is called when a Drag and Drop gesture occurs. Allows you to set the status forDragAndDropTarget
during preparation for this operation;onEntered(event)
,onMoved(event)
,onExited(event)
: these functions are called when an element is located/moved in the areaDragAndDropTarget
;onChanged(event)
: This function is called when the Drag and Drop event changes inDragAndDropTarget
regions;onEnded(event)
: This function is called when the Drag and Drop event is completed. All copiesDragAndDropTarget
in the hierarchy that previously received the eventonStarted
will receive this event, which allows you to reset the state forDragAndDropTarget
.
In our example, PersonCard is the Drop target, so let's add this modifier to the PersonCard.
fun PersonCard(person: Person) {
// состояние для связки элемента еды и человека
val foodItems = remember { mutableStateMapOf<Int, FoodItem>() }
Column(
modifier = Modifier
....
.background(Color.White, RoundedCornerShape(16.dp))
.dragAndDropTarget(
shouldStartDragAndDrop = { event ->
// проверяет, если Drag and Drop содержит текст intent mime типа
event.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_INTENT)
},
target = object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
// достает элемент еды из Drag and Drop события и добавляет его в состояние
val foodItem = event.toAndroidDragEvent().clipData.foodItem() ?: return false
foodItems[foodItem.id] = foodItem
return true
}
}
),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) { // контент карточки PersonCard }
private fun ClipData.foodItem(): FoodItem? {
return (0 until itemCount)
.mapNotNull(::getItemAt).firstNotNullOfOrNull { item ->
item.intent?.getStringExtra(foodItemTransferData)?.takeIf { it.isNotEmpty() }
}?.let { Gson().fromJson(it, FoodItem::class.java) }
}
Inside onDrop
functions, we extract the food element Drag and Drop events and add foodItems
state.
Now for the Drop target to change its color when the source is in the area of the desired Drag and Drop. Need to listen to events onEntered
And onExited
.
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PersonCard(person: Person) {
var bgColor by remember { mutableStateOf(Color.White) }
Column(
modifier = Modifier
....
.background(bgColor, RoundedCornerShape(16.dp))
.dragAndDropTarget(
shouldStartDragAndDrop = { event -> ... },
target = object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
...
bgColor = Color.White
return true
}
override fun onEntered(event: DragAndDropEvent) {
super.onEntered(event)
bgColor = Color.Red
}
override fun onExited(event: DragAndDropEvent) {
super.onExited(event)
bgColor = Color.White
}
}
),
) { /* карточка PersonCard */ }
You can find the source code from the article in the Github repository – https://github.com/cp-radhika-s/Drag_and_drop_jetpack_compose/tree/explore-modifier-drag-drop-source