Вступление
В январе 2024, крупное обновление Jetpack Compose добавило два новых модификатора: dragAndDropSource и dragAndDropTarget. В этой статье я расскажу как реализовать эффект Drag and Drop в Jetpack Compose.
Composable функция для отображения карточки еды
Сперва напишем код для карточки еды.

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 функция для отображения карточки человека
Далее напишем код для карточки человека.

@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) } }
Добавим источник для Drag and Drop
Перед тем как начнём, небольшая справка по модификаторам:
Modifier.dragAndDropSource
Этот модификатор позволяет компоненту стать источником Drag and Drop.
@Composable fun MyDraggableComponent() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .dragAndDropSource( drawDragDecoration = { // UI перетаскиваемого элемента } ) { // логика обработки Drag and Drop startTransfer (/* данные для передачи */ ) } ) { /* контент перетаскиваемого компонента */ } }
Modifier.dragAndDropSourceпринимает два параметра:drawDragDecorationиblock;drawDragDecoration— это лямбда блок, который предоставляет визуальное представление компонента, который перетаскивается Drag and Drop'ом;blockэто лямбда блок, он принимаетDragAndDropSourceScopeкак ресивер. Он позволяет обнаружить Drag and Drop жест и в последствии вы можете его обработать;вызов
startTransferв лямбде инициализирует Drag and Drop действие.
Теперь давайте добавим этот модификатор на карточку еды:
Тут, transferData определяет контент, который должен передаваться при Drag and Drop. Например, передача данных из карточки еды в карточку человека.
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) ) }, ) )
Теперь карточка еды стала перетаскиваемой.
Добавим место для Drop and Drop
Вторая справка по модификаторам:
Modifier.dragAndDropTarget
Этот модификатор в Jetpack Compose по��воляет composable функциям получить события от Drop and Drop.
@Composable fun MyDragTarget() { Box( modifier = Modifier .size(100.dp) .background(Color.Green) .dragAndDropTarget( shouldStartDragAndDrop = { startEvent-> return true }, target = object : DragAndDropTarget { ... } ) ) { /* контент перетаскиваемого компонента */ } }
Он принимает два параметра:
shouldStartDragAndDrop: говорит компоненту нужно ли получать события перетаскивания отDragAndDropEvent.target—DragAndDropTargetполучает следующие события от Drag and Drop:onDrop(event): эта функция вызовется, когда компонент будет перетащен внутрьDragAndDropTarget. Значениеtrueуказывает на то, что событие отDragAndDropEventбыло обработано, напротивfalseуказывает на то, что оно было отклонено;onStarted(event): эта функция вызывается, когда происходит Drag and Drop жест. Позволяет установить состояние дляDragAndDropTargetво время подготовки к этой операции;onEntered(event),onMoved(event),onExited(event): эти функции, вызываются когда элемент находится/двигается в областиDragAndDropTarget;onChanged(event): эта функция вызывается, когда событие Drag and Drop меняется вDragAndDropTargetобласти;onEnded(event): эта функция вызывается, когда событие Drag and Drop завершено. Все экземплярыDragAndDropTargetв иерархии, которые ранее получили событиеonStartedполучат это событие, что позволяет сбросить состояние дляDragAndDropTarget.
В нашем примере PersonCard — это цель Drop, поэтому давайте добавим этот модификатор карточке 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) } }
Внутри onDrop функции, мы извлекаем элемент еды Drag and Drop события и добавляем foodItems состояние.
Теперь, чтобы цель Drop изменила свой цвет, когда источник находится в области желаемого Drag and Drop. Нужно прослушать события onEntered и 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 */ }
Исходный код из статьи вы можете найти в Github репозитории — https://github.com/cp-radhika-s/Drag_and_drop_jetpack_compose/tree/explore-modifier-drag-drop-source
