Реалізація анімації перетягування (Drag & Reorder) у мобільних додатках
Користувач довго утримує елемент списку — той піднімається, масштабується, а сусідні елементи плавно розсуваються, показуючи місце для вставки. Це анімація Drag & Reorder. Без неї не обходиться жоден додаток із сортованими списками: від задачників до інтернет-магазинів. Але реалізація на різних платформах має особливості, які можуть зламати UX. Особливо часті проблеми на проектах з високим навантаженням: перетягування 1000+ елементів потребує тротлінгу анімацій. Наші рішення перевірені в production.
Ми впровадили Drag & Reorder у 15+ проектах за 5 років досвіду. Ось ключові проблеми: відсутність стабільних ключів призводить до «стрибків» при реордерингу; мутація списку без повідомлення фреймворку викликає затримки оновлення; ігнорування оптимістичного оновлення загрожує втратою нового порядку при збої мережі. Це дозволяє заощадити до 30% часу розробки порівняно з кастомною реалізацією. Зниження витрат на підтримку на 20% при використанні готових API. Розберемо, як уникнути цих помилок.
Чому стабільні ключі критичні для анімації?
Кожен елемент списку повинен мати унікальний і стабільний ідентифікатор. Без цього фреймворк не може коректно відстежувати переміщення та анімувати розсування. Наприклад, у SwiftUI використовуйте протокол Identifiable, у Compose — key параметр у .items(), у Flutter — ValueKey при побудові елементів. Якщо ключ змінюється при реордерингу (наприклад, на основі позиції), анімація ламається.
Як вибрати між вбудованими API та кастомною реалізацією?
Вбудовані API (iOS UIKit, SwiftUI .onMove, Flutter ReorderableListView) покривають 80% сценаріїв. Кастомна реалізація потрібна, якщо:
- потрібна нестандартна анімація піднятого елемента (наприклад, підйом з тінню, зміна кольору);
- список містить складні комірки з вкладеними жестами;
- необхідна інтеграція з drag-and-drop між різними списками або вкладками.
Ми рекомендуємо починати з вбудованих API та додавати кастомізацію лише за необхідності. Це прискорює розробку та спрощує тестування.
Як впровадити Drag & Reorder за 4 кроки?
- Вибір API. iOS (UIKit) — вбудовані UICollectionViewDragDelegate/UICollectionViewDropDelegate. Android Compose — бібліотека reorderable. Flutter — ReorderableListView.
- Налаштування анімації. Піднятий елемент масштабуємо та піднімаємо за допомогою shadow. На iOS це відбувається автоматично; на Android використовуємо animateFloatAsState, на Flutter — proxyDecorator.
- Збереження порядку. Після завершення перетягування відправляємо новий масив на backend. Застосовуємо оптимістичне оновлення: UI змінюється одразу, при помилці мережі — відкат.
- Тестування. Перевіряємо на пристроях з різними версіями ОС, особливо на старих iOS (12 і вище) та Android (API 26+).
Як реалізувати Drag & Reorder на iOS (UIKit)?
Згідно з Apple Human Interface Guidelines, анімація перетягування має бути плавною та передбачуваною. Використовуємо UICollectionView з делегатами:
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 автоматично анімує переміщення комірок. Для UITableView підхід аналогічний з UITableViewDragDelegate/UITableViewDropDelegate. У SwiftUI використовуйте .onMove — він додає drag handles, але без кастомізації зовнішнього вигляду.
Як анімувати перетягування в Android Compose?
У 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)
.padding(16.dp)
)
}
}
}
}
}
isDragging керує анімацією піднятого елемента. Для кастомної анімації розсування сусідніх елементів на Compose 1.7+ використовуйте Modifier.animateItem().
Flutter: ReorderableListView з proxyDecorator
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 дозволяє підняти перетягуваний елемент без зміни оригінального віджету.
Порівняння платформ: що вибрати?
| Платформа | Вбудована підтримка | Час реалізації (простий) | Час реалізації (кастомний) |
|---|---|---|---|
| iOS (UIKit) | Так (з iOS 11) | 2-3 години | 1 день |
| SwiftUI | Так (.onMove) | 1 година | 0,5 дня |
| Android Compose | Ні (потрібна бібліотека) | 3-4 години | 1 день |
| Flutter | Так (ReorderableListView) | 2 години | 0,5 дня |
iOS вирішується вдвічі швидше за Android Compose, якщо використовувати вбудовані API. Flutter — золота середина за швидкістю та гнучкістю.
Порівняння анімаційних можливостей
| Платформа | Підняття елемента | Розсування сусідів | Кастомізація |
|---|---|---|---|
| iOS UIKit | Автоматично (scale + shadow) | Автоматично | Обмежена |
| SwiftUI | Через .onMove з dragPreview | Через withAnimation | Середня |
| Android Compose | Ручна (animateFloatAsState) | Через animateItem() | Висока |
| Flutter | Через proxyDecorator | Автоматично | Висока |
Що входить у роботу
При замовленні реалізації Drag & Reorder під ключ ми надаємо:
- Аналіз поточного списку та вибір оптимального API для вашої платформи
- Реалізацію анімації підняття та розсування елементів
- Інтеграцію з backend для збереження нового порядку (включаючи оптимістичне оновлення)
- Тестування на реальних пристроях з різними версіями ОС
- Документацію щодо впровадження та підтримки
Наші інженери з 5-річним досвідом гарантують якісну реалізацію. Замовте впровадження анімації перетягування та отримайте консультацію щодо вашого проекту.
Типові помилки
- Відсутність стабільних ключів — елементи без
keyпризводять до того, що фреймворк не може коректно анімувати переміщення. Завжди використовуйтеkey: ValueKey(item.id)(Flutter),idу.items(key=...)(Compose) або реалізуйтеHashable(SwiftUI). - Мутація списку без повідомлення — у Compose обов'язково використовуйте
mutableStateOfта створюйте новий список при мутації. У Flutter —setState. В iOS —performBatchUpdatesз move. - Ігнорування збереження порядку — після
onReorderодразу відправляйте новий порядок на backend. Використовуйте оптимістичне оновлення: UI оновлюється миттєво, при помилці мережі — відкат.
Докладніше про драг-енд-дроп на iOS читайте в офіційній документації Apple. Для Android Compose — Android Developers.
Строки та вартість
Drag & Reorder для простого списку (використовуємо вбудовані API) — півдня. Кастомний drag з анімованим проксівидом, розсуванням сусідніх елементів та інтеграцією зі сховищем — 1-2 дні. Вартість розраховується індивідуально. Наші сертифіковані інженери гарантують якість — понад 5 років досвіду. Отримайте консультацію або замовте реалізацію вже сьогодні.







