В первой части мы рассмотрели 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.