Drag и Swipe в RecyclerView. Часть 2: контроллеры перетаскивания, сетки и пользовательские анимации

Drag и Swipe в RecyclerView. Часть 2: контроллеры перетаскивания, сетки и пользовательские анимации

В первой части мы рассмотрели ItemTouchHelper и реализацию ItemTouchHelper.Callback, которая добавляет базовые функции drag & drop и swipe-to-dismiss в RecyclerView. В этой статье мы продолжим то, что было сделано в предыдущей, добавив поддержку расположения элементов в виде сетки, контроллеры перетаскивания, выделение элемента списка и пользовательские анимации смахивания (англ. swipe).

Сетка элементов

Контроллеры перетаскивания

При создании списка, поддерживающего drag & drop, обычно реализуют возможность перетаскивания элементов по касанию. Это способствует понятности и удобству использования списка в «режиме редактирования», а также рекомендуется material-гайдлайнами. Добавить контроллеры перетаскивания в наш пример сказочно легко.

Контроллеры перетаскивания

Сперва обновим layout элемента (item_main.xml).

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@+id/item"     android:layout_width="match_parent"     android:layout_height="?listPreferredItemHeight"     android:clickable="true"     android:focusable="true"     android:foreground="?selectableItemBackground">      <TextView         android:id="@+id/text"         android:layout_width="wrap_content"         android:layout_height="wrap_content"         android:layout_gravity="center_vertical"         android:layout_marginLeft="16dp"         android:textAppearance="?android:attr/textAppearanceMedium" />      <ImageView         android:id="@+id/handle"         android:layout_width="?listPreferredItemHeight"         android:layout_height="match_parent"         android:layout_gravity="center_vertical|right"         android:scaleType="center"         android:src="@drawable/ic_reorder_grey_500_24dp" /> </FrameLayout>

Изображение, используемое для контроллера перетаскивания, можно найти в Material Design иконках и добавить в проект с помощью удобного плагина генератора иконок в Android Studio.

Как кратко упоминалось в прошлой статье, вы можете использовать ItemTouchHelper.startDrag(ViewHolder), чтобы программно запустить перетаскивание. Итак, всё, что нам нужно сделать, это обновить ViewHolder, добавив контроллер перетаскивания, и настроить простой обработчик касаний, который будет вызывать startDrag().

Нам понадобится интерфейс для передачи события по цепочке:

public interface OnStartDragListener {      /**      * Called when a view is requesting a start of a drag.      *      * @param viewHolder The holder of the view to drag.      */     void onStartDrag(RecyclerView.ViewHolder viewHolder); }

Затем определите ImageView для контроллера перетаскивания в ItemViewHolder:

public final ImageView handleView; public ItemViewHolder(View itemView) {     super(itemView);     // ...     handleView = (ImageView) itemView.findViewById(R.id.handle); }

и обновите RecyclerListAdapter:

private final OnStartDragListener mDragStartListener;  public RecyclerListAdapter(OnStartDragListener dragStartListener) {     mDragStartListener = dragStartListener;     // ... } @Override public void onBindViewHolder(final ItemViewHolder holder,          int position) {     // ...     holder.handleView.setOnTouchListener(new OnTouchListener() {         @Override         public boolean onTouch(View v, MotionEvent event) {             if (MotionEventCompat.getActionMasked(event) ==                      MotionEvent.ACTION_DOWN) {                 mDragStartListener.onStartDrag(holder);             }             return false;         }     }); }

RecyclerListAdapter теперь должен выглядеть примерно так.

Всё, что осталось сделать, это добавить OnStartDragListener во фрагмент:

public class RecyclerListFragment extends Fragment implements          OnStartDragListener {      // ...     @Override     public void onViewCreated(View view, Bundle icicle) {         super.onViewCreated(view, icicle);          RecyclerListAdapter a = new RecyclerListAdapter(this);         // ...     }     @Override     public void onStartDrag(RecyclerView.ViewHolder viewHolder) {         mItemTouchHelper.startDrag(viewHolder);     } }

RecyclerListFragment теперь должен выглядеть следующим образом. Теперь, когда вы запустите приложение, то сможете начать перетаскивание, коснувшись контроллера.

Перетаскивание элементов списка

Выделение элемента списка

Сейчас в нашем примере нет никакой визуальной индикации элемента, который перетаскивается. Очевидно, так быть не должно, но это легко исправить. С помощью ItemTouchHelper можно использовать стандартные эффекты подсветки элемента. На Lollipop и более поздних версиях Android, подсветка «расплывается» по элементу в процессе взаимодействия с ним; на более ранних версиях элемент просто меняет свой цвет на затемнённый.

Чтобы реализовать это в нашем примере, просто добавьте фон (свойство background) в корневой FrameLayout элемента item_main.xml или установите его в конструкторе RecyclerListAdapter.ItemViewHolder. Это будет выглядеть примерно так:

Выделение элемента

Выглядит круто, но, возможно, вы захотите контролировать ещё больше. Один из способов сделать это — позволить ViewHolder обрабатывать изменения состояния элемента. Для этого ItemTouchHelper.Callback предоставляет ещё два метода:

  • onSelectedChanged(ViewHolder, int) вызывается каждый раз, когда состояние элемента меняется на drag (ACTION_STATE_DRAG) или swipe (ACTION_STATE_SWIPE). Это идеальное место, чтобы изменить состояние view-компонента на активное.
  • clearView(RecyclerView, ViewHolder) вызывается при окончании перетаскивания view-компонента, а также при завершении смахивания (ACTION_STATE_IDLE). Здесь обычно восстанавливается изначальное состояние вашего view-компонента.

А теперь давайте просто соберём всё это вместе.

Сперва создайте интерфейс, который будут реализовывать ViewHolders:

/**  * Notifies a View Holder of relevant callbacks from   * {@link ItemTouchHelper.Callback}.  */ public interface ItemTouchHelperViewHolder {      /**      * Called when the {@link ItemTouchHelper} first registers an       * item as being moved or swiped.      * Implementations should update the item view to indicate       * it's active state.      */     void onItemSelected();      /**      * Called when the {@link ItemTouchHelper} has completed the       * move or swipe, and the active item state should be cleared.      */     void onItemClear(); }

Затем в SimpleItemTouchHelperCallback реализуйте соотвутствующие методы:

@Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder,          int actionState) {    // We only want the active item    if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {         if (viewHolder instanceof ItemTouchHelperViewHolder) {             ItemTouchHelperViewHolder itemViewHolder =                      (ItemTouchHelperViewHolder) viewHolder;             itemViewHolder.onItemSelected();         }     }      super.onSelectedChanged(viewHolder, actionState); } @Override public void clearView(RecyclerView recyclerView,          RecyclerView.ViewHolder viewHolder) {     super.clearView(recyclerView, viewHolder);      if (viewHolder instanceof ItemTouchHelperViewHolder) {         ItemTouchHelperViewHolder itemViewHolder =                  (ItemTouchHelperViewHolder) viewHolder;         itemViewHolder.onItemClear();     } }

Теперь осталось только, чтобы RecyclerListAdapter.ItemViewHolder реализовал ItemTouchHelperViewHolder:

public class ItemViewHolder extends RecyclerView.ViewHolder          implements ItemTouchHelperViewHolder {      // ...     @Override     public void onItemSelected() {         itemView.setBackgroundColor(Color.LTGRAY);     }      @Override     public void onItemClear() {         itemView.setBackgroundColor(0);     } }

В этом примере мы просто добавляем серый фон во время активности элемента, а затем его удаляем. Если ваш ItemTouchHelper и адаптер тесно связаны, вы можете легко отказаться от этой настройки и переключать состояние view-компонента прямо в ItemTouchHelper.Callback.

Сетки

Если теперь вы попытаетесь использовать GridLayoutManager, вы увидите, что он работает неправильно. Причина и решение проблемы просты: мы должны сообщить нашему ItemTouchHelper, что мы хотим поддерживать перетаскивание элементов влево и вправо. Ранее в SimpleItemTouchHelperCallback мы уже указывали:

@Override public int getMovementFlags(RecyclerView recyclerView,          RecyclerView.ViewHolder viewHolder) {     int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;     int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;     return makeMovementFlags(dragFlags, swipeFlags); }

Единственное изменение, необходимое для поддержки сеток, заключается в добавлении соответствующих флагов:

int dragFlags = ItemTouchHelper.UP   | ItemTouchHelper.DOWN |                  ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;

Тем не менее, swipe-to-dismiss не очень естественное поведение для элементов в виде сетки, поэтому swipeFlags разумнее всего обнулить:

@Override public int getMovementFlags(RecyclerView recyclerView,          RecyclerView.ViewHolder viewHolder) {     int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN |                      ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;     int swipeFlags = 0;     return makeMovementFlags(dragFlags, swipeFlags); }

Чтобы увидеть рабочий пример GridLayoutManager, смотрите RecyclerGridFragment. Вот как это выглядит при запуске:

Элементы в виде сетки

Пользовательские анимации смахивания

ItemTouchHelper.Callback предоставляет действительно удобный способ для полного контроля анимации во время перетаскивания или смахивания. Поскольку ItemTouchHelper — это RecyclerView.ItemDecoration, мы можем вмешаться в процесс отрисовки view-компонента похожим образом. В следующей части мы разберём этот вопрос подробнее, а пока посмотрим на простой пример переопределения анимации смахивания по умолчанию, чтобы показать линейное исчезновение.

@Override public void onChildDraw(Canvas c, RecyclerView recyclerView,          ViewHolder viewHolder, float dX, float dY,          int actionState, boolean isCurrentlyActive) {      if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {         float width = (float) viewHolder.itemView.getWidth();         float alpha = 1.0f - Math.abs(dX) / width;         viewHolder.itemView.setAlpha(alpha);         viewHolder.itemView.setTranslationX(dX);         } else {         super.onChildDraw(c, recyclerView, viewHolder, dX, dY,                  actionState, isCurrentlyActive);     } }

Параметры dX и dY — это текущий сдвиг относительно выделенного view-компонента, где:

  • -1.0f — это полное смахивание справа налево (от ItemTouchHelper.END к ItemTouchHelper.START)
  • 1.0f — это полное смахивание слева направо (от ItemTouchHelper.START к ItemTouchHelper.END)

Важно вызывать super для любого actionState, который вы не обрабатываете, для того, чтобы запускалась анимация по умолчанию.

В следующей части мы рассмотрим пример, в котором будем контролировать отрисовку элемента в момент перетаскивания.

Заключение

На самом деле, настройка ItemTouchHelper — это довольно весело. Чтобы не увеличивать объём этой статьи, я разделил её на несколько.

Исходный код

Весь код этой серии статей смотрите на GitHub-репозитории Android-ItemTouchHelper-Demo. Эта статья охватывает коммиты от ef8f149 до d164fba.

← Drag и Swipe в RecyclerView. Часть 1: ItemTouchHelper

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *