Реализация анимации перетаскивания (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 лет опыта. Получите консультацию или закажите реализацию уже сегодня.







