Реализация анимации перетаскивания элементов (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's item placement animation.

Для кастомной анимации расступания — 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 — виджет-заменитель для перетаскиваемого элемента. Через него добавляем 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 дня. Стоимость рассчитывается индивидуально.