Вступление
В январе 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