Drag and Drop в Jetpack Compose

от автора

Вступление

В январе 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 { ... }             )     ) { /* контент перетаскиваемого компонента */ } }

Он принимает два параметра:

  1. shouldStartDragAndDrop: говорит компоненту нужно ли получать события перетаскивания от DragAndDropEvent.

  2. target — DragAndDropTarget получает следующие события от Drag and Drop:

    1. onDrop(event): эта функция вызовется, когда компонент будет перетащен внутрь DragAndDropTarget. Значение true указывает на то, что событие от DragAndDropEvent было обработано, напротив false указывает на то, что оно было отклонено;

    2. onStarted(event): эта функция вызывается, когда происходит Drag and Drop жест. Позволяет установить состояние для DragAndDropTarget во время подготовки к этой операции;

    3. onEntered(event)onMoved(event)onExited(event): эти функции, вызываются когда элемент находится/двигается в области DragAndDropTarget;

    4. onChanged(event): эта функция вызывается, когда событие Drag and Drop меняется в DragAndDropTarget области;

    5. 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


ссылка на оригинал статьи https://habr.com/ru/articles/800389/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *