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 And block;

  • 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 accepts DragAndDropSourceScope 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:

  1. shouldStartDragAndDrop: tells the component whether to receive drag events from DragAndDropEvent.

  2. targetDragAndDropTarget receives the following events from Drag and Drop:

    1. onDrop(event): This function will be called when a component is dragged inside DragAndDropTarget. Meaning true indicates that the event is from DragAndDropEvent was processed, on the contrary false indicates that it has been rejected;

    2. onStarted(event): This function is called when a Drag and Drop gesture occurs. Allows you to set the status for DragAndDropTarget during preparation for this operation;

    3. onEntered(event), onMoved(event), onExited(event): these functions are called when an element is located/moved in the area DragAndDropTarget;

    4. onChanged(event): This function is called when the Drag and Drop event changes in DragAndDropTarget regions;

    5. onEnded(event): This function is called when the Drag and Drop event is completed. All copies DragAndDropTarget in the hierarchy that previously received the event onStarted will receive this event, which allows you to reset the state for DragAndDropTarget.

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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *