Реалізація анімації перетягування елементів (Drag & Reorder) у мобільному додатку

TRUETECH займається розробкою, підтримкою та обслуговуванням мобільних додатків iOS, Android, PWA. Маємо великий досвід та експертизу для публікації мобільних додатків до популярних маркетів Google Play, App Store, Amazon, AppGallery та інші.

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1735 послуг
Реалізація анімації перетягування елементів (Drag & Reorder) у мобільному додатку
Середній
від 1 дня до 3 днів
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_mobile-applications_feedme_467_0.webp
    Розробка мобільного додатка для компанії FEEDME
    792
  • image_mobile-applications_xoomer_471_0.webp
    Розробка мобільного додатку для компанії XOOMER
    671
  • image_mobile-applications_rhl_428_0.webp
    Розробка мобільного додатку для компанії RHL
    1097
  • image_mobile-applications_zippy_411_0.webp
    Розробка мобільного додатку для компанії ZIPPY
    969
  • image_mobile-applications_affhome_429_0.webp
    Розробка мобільного додатку для компанії Affhome
    914
  • image_mobile-applications_flavors_409_0.webp
    Розробка мобільного додатку для компанії FLAVORS
    495

Реалізація анімації Drag & Reorder у мобільних додатках

Drag & Reorder дозволяє користувачам перетягувати елементи списку для зміни їх порядку. Користувач довго натискає на елемент; список розуміє намір (елемент піднімається і трохи масштабується); потім працює drag: інші елементи розступаються, показуючи місце вставки.

Технічно це один із найскладніших списків анімацій: відстежувати drag у реальному часі, обчислювати цільову позицію вставки, анімувати сусідні елементи без перекомпозиції всього списку.

iOS: UICollectionView з реордерингом

UICollectionView має вбудовану підтримку інтерактивного реордерингу через UICollectionViewDragDelegate та UICollectionViewDropDelegate (iOS 11+):

collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true

// UICollectionViewDragDelegate:
func collectionView(_ collectionView: UICollectionView,
                    itemsForBeginning session: UIDragSession,
                    at indexPath: IndexPath) -> [UIDragItem] {
    let item = items[indexPath.item]
    let itemProvider = NSItemProvider(object: item.id as NSString)
    let dragItem = UIDragItem(itemProvider: itemProvider)
    dragItem.localObject = item
    return [dragItem]
}

// UICollectionViewDropDelegate:
func collectionView(_ collectionView: UICollectionView,
                    performDropWith coordinator: UICollectionViewDropCoordinator) {
    guard let destinationIndexPath = coordinator.destinationIndexPath,
          let item = coordinator.items.first,
          let sourceIndexPath = item.sourceIndexPath else { return }

    collectionView.performBatchUpdates {
        items.move(fromOffsets: IndexSet(integer: sourceIndexPath.item),
                   toOffset: destinationIndexPath.item)
        collectionView.moveItem(at: sourceIndexPath, to: destinationIndexPath)
    }
    coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
}

UICollectionView автоматично анімує перемещення сусідніх ячеєк — це найцінніше. Не потрібно вручну рахувати й анімувати offset кожного елемента.

Для UITableView — аналогічно через UITableViewDragDelegate/UITableViewDropDelegate або через старий підхід з editingStyle:

tableView.isEditing = true
// Delegate method:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    items.move(fromOffsets: IndexSet(integer: sourceIndexPath.row),
               toOffset: destinationIndexPath.row)
}

У SwiftUI — List з .onMove:

List {
    ForEach(items) { item in
        ItemRow(item: item)
    }
    .onMove { source, destination in
        items.move(fromOffsets: source, toOffset: destination)
    }
}
.environment(\.editMode, .constant(.active))

.onMove додає стандартні drag handles. Для кастомного long-press drag без edit mode — DragGesture з .onChanged та .onEnded, плюс вручну вичислення цільового індекса. Складніше, але дає повний контроль над видом.

Android Compose: LazyColumn + reorderable

Compose немає вбудованого reorder — використовуємо бібліотеку sh.calvin.reorderable:reorderable:2.4.0:

val listState = rememberLazyListState()
var list by remember { mutableStateOf(items) }
val reorderState = rememberReorderableLazyListState(listState) { from, to ->
    list = list.toMutableList().apply { add(to.index, removeAt(from.index)) }
}

LazyColumn(
    state = listState,
    modifier = Modifier.reorderable(reorderState)
) {
    items(list, key = { it.id }) { item ->
        ReorderableItem(reorderState, key = item.id) { isDragging ->
            val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
            val scale by animateFloatAsState(if (isDragging) 1.05f else 1f)

            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .scale(scale)
                    .shadow(elevation),
            ) {
                Row {
                    Text(item.title, modifier = Modifier.weight(1f).padding(16.dp))
                    Icon(
                        Icons.Default.DragHandle,
                        contentDescription = null,
                        modifier = Modifier
                            .detectReorderAfterLongPress(reorderState)  // або draggableHandle()
                            .padding(16.dp)
                    )
                }
            }
        }
    }
}

isDragging дозволяє анімувати піднятий елемент — scale та shadow через animateFloatAsState та animateDpAsState. Інші елементи автоматично анімуються через анімацію розміщення LazyColumn.

Для кастомної анімації розступання — Modifier.animateItem() на Compose 1.7+:

items(list, key = { it.id }) { item ->
    ItemRow(item, modifier = Modifier.animateItem())
}

animateItem() автоматично анімує появу, зникнення та зміщення елементів у LazyColumn при зміні списку.

Flutter

// pubspec: reorderable_list: ^0.2.2 або вбудований ReorderableListView
ReorderableListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      key: ValueKey(items[index].id),
      title: Text(items[index].title),
      trailing: ReorderableDragStartListener(
        index: index,
        child: const Icon(Icons.drag_handle),
      ),
    );
  },
  onReorder: (oldIndex, newIndex) {
    setState(() {
      if (newIndex > oldIndex) newIndex--;
      final item = items.removeAt(oldIndex);
      items.insert(newIndex, item);
    });
  },
  proxyDecorator: (child, index, animation) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        final scale = Tween<double>(begin: 1.0, end: 1.05)
            .evaluate(CurvedAnimation(parent: animation, curve: Curves.easeOut));
        return Transform.scale(scale: scale, child: child);
      },
      child: child,
    );
  },
)

proxyDecorator — widget-заміна для перетаскуваного елемента. Через нього додаємо scale та shadow ефект без зміни оригінального ListTile.

Типові помилки

Списки без key на елементах — при реординге framework не може зіставити старі та нові позиції, анімація «прискорюється» або ломається. key: ValueKey(item.id) обов'язковий.

Мутація списку без сповіщення framework — у Compose mutableStateOf з list.toMutableList() перед мутацією. У Flutter — setState. Без цього UI не оновлюється.

Збереження нового порядку: після onReorder — одразу відправляємо новий порядок на backend або локальне сховище. Якщо користувач уйде — порядок збережений. Оптимістичне оновлення: застосовуємо до UI одразу, rollback при помилці мережі.

Терміни

Drag & Reorder для простого списку через вбудовані API (UITableView, ReorderableListView): половина дня. Кастомний drag з анімованим проксивідом, розступанням сусідніх елементів та збереженням у сховищі: 1–2 дні. Вартість розраховується індивідуально.