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







