Статьи

Добавление жестов смахивания в RecyclerViews

Большая часть Material Design — это способ взаимодействия пользователей с визуальными элементами приложения. Поэтому в дополнение к касаниям и долгим нажатиям сегодня ожидается, что хорошо сделанное приложение для Android будет обрабатывать более сложные жесты, такие как перетаскивание и перетаскивание. Это особенно важно, если приложение использует списки для отображения своих данных.

С помощью виджета RecyclerView и нескольких других компонентов Android Jetpack вы можете обрабатывать различные жесты, связанные со списком, в своих приложениях. Кроме того, всего за несколько строк кода вы можете связать анимацию Material Motion с этими жестами.

В этом уроке я покажу вам, как добавить несколько общих жестов смахивания, в комплекте с интуитивно понятными анимациями, в ваши списки.

Чтобы максимально использовать этот урок, вам понадобятся:

  • Android Studio 3.2.1 или выше
  • телефон или планшет под управлением Android API уровня 23 или выше

Чтобы этот урок был коротким, давайте сгенерируем один из шаблонов, доступных в Android Studio.

Начните с запуска Android Studio и создания нового проекта. В мастере создания проекта убедитесь, что вы выбрали опцию « Пустое действие» .

Project creation wizard

Вместо библиотеки поддержки мы будем использовать Android Jetpack в этом проекте. Итак, после создания проекта перейдите в Refactor> Migrate to AndroidX . При появлении запроса нажмите кнопку « Перенос» .

Запрос подтверждения для продолжения миграции

Затем, чтобы добавить список в проект, выберите «Файл»> «Создать»> «Фрагмент»> «Фрагмент (список)» . В появившемся диалоговом окне продолжите и нажмите кнопку Готово , не внося никаких изменений в значения по умолчанию.

Мастер создания фрагмента списка

На этом этапе Android Studio создаст новый фрагмент, содержащий полностью настроенный виджет RecyclerView . Он также будет генерировать фиктивные данные для отображения внутри виджета. Тем не менее, вам все равно придется добавить фрагмент к основному виду деятельности вручную.

Для этого сначала добавьте интерфейс OnListFragmentInteractionListener к основному OnListFragmentInteractionListener деятельности и реализуйте единственный содержащийся в нем метод.

1
2
3
4
override fun onListFragmentInteraction(
                            item: DummyContent.DummyItem?) {
   // leave empty
}

Затем вставьте фрагмент в действие, добавив следующий <fragment> в файл activity_main.xml :

1
2
3
4
<fragment android:layout_width=»match_parent»
         android:layout_height=»match_parent»
         android:id=»@+id/list_fragment»
         android:name=»com.tutsplus.rvswipes.ItemFragment» />

На этом этапе, если вы запустите свое приложение, вы сможете увидеть список, который выглядит следующим образом:

Приложение отображает простой список

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

Класс ItemTouchHelper нуждается в экземпляре абстрактного класса ItemTouchHelper.Callback чтобы иметь возможность обнаруживать и обрабатывать жесты. Хотя вы можете использовать его напрямую, гораздо проще использовать вместо этого класс-оболочку SimpleCallback . Это тоже абстрактно, но у вас будет меньше методов для переопределения.

Создайте новый экземпляр класса SimpleCallback внутри onCreateView() класса ItemFragment . В качестве аргумента для его конструктора вы должны передать направление свайпа, которое хотите обработать. А пока передайте ему RIGHT чтобы он обрабатывал жест смахивания вправо.

1
2
3
4
5
6
val myCallback = object: ItemTouchHelper.SimpleCallback(0,
                                 ItemTouchHelper.RIGHT) {
                                  
    // More code here
     
}

У класса есть два абстрактных метода, которые вы должны переопределить: метод onMove() , который обнаруживает перетаскивания, и метод onSwiped() , который обнаруживает свайпы. Поскольку сегодня мы не будем обрабатывать какие-либо жесты перетаскивания, убедитесь, что вы возвращаете false в onMove() .

01
02
03
04
05
06
07
08
09
10
11
12
override fun onMove(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    target: RecyclerView.ViewHolder
): Boolean = false
 
override fun onSwiped(viewHolder: RecyclerView.ViewHolder,
                      direction: Int) {
 
    // More code here
 
}

Внутри onSwiped() вы можете использовать свойство adapterPosition чтобы определить индекс элемента списка, который был проведен. Поскольку сейчас мы реализуем жест смахивания для удаления, передайте индекс методу removeAt() из фиктивного списка, чтобы удалить элемент.

1
DummyContent.ITEMS.removeAt(viewHolder.adapterPosition)

Кроме того, вы должны передать этот же индекс notifyItemRemoved() адаптера виджета RecyclerView , чтобы убедиться, что элемент больше не отображается. При этом также запускается анимация удаления элемента по умолчанию.

1
adapter?.notifyItemRemoved(viewHolder.adapterPosition)

На этом этапе объект SimpleCallback готов. Все, что вам нужно сделать сейчас, это создать объект ItemTouchHelper и прикрепить к нему виджет RecyclerView .

1
2
val myHelper = ItemTouchHelper(myCallback)
myHelper.attachToRecyclerView(this)

Если вы запустите приложение сейчас, вы сможете смахивать элементы из списка.

Хотя жест смахивания с удалением очень интуитивен, некоторые пользователи могут быть не уверены, что произойдет, когда они будут выполнять жест. Поэтому в руководствах по дизайну материалов говорится, что жест должен также постепенно раскрывать вид, скрытый за элементом, который четко указывает на то, что произойдет дальше. Обычно фоновый вид — это просто значок, отображающий мусорное ведро.

Чтобы добавить значок корзины в проект, выберите « Файл»> «Создать»> «Векторный набор» и выберите значок « Удалить» .

Диалог выбора иконки

Теперь вы можете получить ссылку на значок в своем коде Kotlin, вызвав метод getDrawable() . Поэтому добавьте следующую строку в метод onCreateView() класса ItemFragment :

1
2
3
4
val trashBinIcon = resources.getDrawable(
                       R.drawable.ic_delete_black_24dp,
                       null
                  )

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

Переопределите метод onChildDraw() вашей реализации SimpleCallback чтобы начать рисование.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
override fun onChildDraw(
    c: Canvas,
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    dX: Float,
    dY: Float,
    actionState: Int,
    isCurrentlyActive: Boolean
) {
     
    // More code here
     
    super.onChildDraw(c, recyclerView, viewHolder,
                dX, dY, actionState, isCurrentlyActive)
}

В приведенном выше коде onChildDraw() метода onChildDraw() суперкласса. Без этого ваши элементы списка не будут перемещаться при их перелистывании.

Поскольку мы обрабатываем только жест смахивания вправо, координаты X верхнего левого и нижнего левого углов фонового представления всегда будут равны нулю. Координаты X верхнего правого и нижнего правого углов, с другой стороны, должны быть равны параметру dX , который указывает, насколько элемент списка был перемещен пользователем.

Чтобы определить координаты Y всех углов, вы должны будете использовать свойства top и bottom одного из представлений, представленных внутри объекта viewHolder .

Используя все эти координаты, теперь вы можете определить прямоугольную область клипа. В следующем коде показано, как использовать для clipRect() метод clipRect() объекта Canvas :

1
2
c.clipRect(0f, viewHolder.itemView.top.toFloat(),
       dX, viewHolder.itemView.bottom.toFloat())

Несмотря на то, что это не обязательно, рекомендуется сделать область клипа видимой, придав ей цвет фона. Вот как вы можете использовать метод drawColor() чтобы сделать область клипа серой, когда расстояние drawColor() мала, и красной, когда она больше.

1
2
3
4
if(dX < width / 3)
    c.drawColor(Color.GRAY)
else
    c.drawColor(Color.RED)

Теперь вам нужно будет указать границы значка корзины. Эти границы должны включать поле, соответствующее тексту, отображаемому в элементах списка. Чтобы определить значение поля в пикселях, используйте getDimension() и передайте ему text_margin .

1
2
val textMargin = resources.getDimension(R.dimen.text_margin)
                         .roundToInt()

Вы можете повторно использовать координаты верхнего левого угла области клипа в качестве координат верхнего левого угла значка. Однако они должны быть смещены с помощью textMargin . Чтобы определить координаты его нижнего правого угла, используйте его ширину и высоту. Вот как:

1
2
3
4
5
6
7
trashBinIcon.bounds = Rect(
    textMargin,
    viewHolder.itemView.top + textMargin,
    textMargin + trashBinIcon.intrinsicWidth,
    viewHolder.itemView.top + trashBinIcon.intrinsicHeight
                            + textMargin
)

Наконец, нарисуйте значок, вызвав его метод draw() .

1
trashBinIcon.draw(c)

Если вы снова запустите приложение, вы сможете увидеть значок, когда проводите пальцем, чтобы удалить элемент списка.

Жест смахивания до обновления, также известный как жест с вытягиванием до обновления, стал настолько популярным в наши дни, что в Android Jetpack есть специальный компонент для него. Он называется SwipeRefreshLayout и позволяет быстро связать жест с любым виджетом RecyclerView , ListView или GridView .

Для поддержки жеста смахивания до обновления в вашем виджете RecyclerView вы должны сделать его дочерним по SwipeRefreshLayout виджету SwipeRefreshLayout . Итак, откройте файл <SwipeRefreshLayout> добавьте к нему тег <SwipeRefreshLayout> и переместите <RecyclerView> внутри него. После этого содержимое файла должно выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<?xml version=»1.0″ encoding=»utf-8″?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        xmlns:android=»https://schemas.android.com/apk/res/android»
        xmlns:app=»http://schemas.android.com/apk/res-auto»
        xmlns:tools=»http://schemas.android.com/tools»
        android:layout_width=»match_parent»
        android:layout_height=»match_parent»>
 
    <androidx.recyclerview.widget.RecyclerView
            android:id=»@+id/list»
            android:name=»com.tutsplus.rvswipes.ItemFragment»
            android:layout_width=»match_parent»
            android:layout_height=»match_parent»
            android:layout_marginLeft=»16dp»
            android:layout_marginRight=»16dp»/>
 
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

Фрагмент списка предполагает, что виджет RecyclerView является корневым элементом его макета. Поскольку это больше не так, вам нужно внести несколько изменений в метод onCreateView() класса ItemFragment . Сначала замените первую строку метода, которая раздувает макет, следующим кодом:

1
2
3
4
5
6
val srLayout: SwipeRefreshLayout =
            inflater.inflate(
               R.layout.fragment_item_list, container, false
            ) as SwipeRefreshLayout
 
val view = srLayout.findViewById<RecyclerView>(R.id.list)

Затем измените последнюю строку метода, чтобы он возвращал виджет SwipeRefreshLayout вместо виджета RecyclerView .

1
return srLayout

Если вы попробуете запустить приложение сейчас, вы сможете выполнить жест вертикальной прокрутки и получить визуальную обратную связь. Содержимое списка не изменится. Чтобы действительно обновить список, вы должны связать объект SwipeRefreshLayout виджетом SwipeRefreshLayout .

1
2
3
srLayout.setOnRefreshListener {
    // More code here
}

Внутри слушателя вы можете изменять данные, отображаемые в списке, в соответствии с вашими требованиями. На данный момент, поскольку мы работаем с фиктивными данными, давайте просто очистим список фиктивных элементов и загрузим его с 25 новыми фиктивными элементами. Следующий код показывает вам, как это сделать:

1
2
3
4
5
6
DummyContent.ITEMS.clear()
for(i in 1..25) {
    DummyContent.ITEMS.add(
        DummyContent.DummyItem(«$i», «Item $i», «»)
    )
}

После обновления данных не забудьте вызвать метод notifyDataSetChanged() чтобы adapter виджета RecyclerView знал, что он должен перерисовать список.

1
view.adapter?.notifyDataSetChanged()

По умолчанию, как только пользователь выполняет жест смахивания для обновления, виджет SwipeRefreshLayout отображает анимированный индикатор хода выполнения. Поэтому после обновления списка вы должны не забыть удалить индикатор, установив для свойства isRefreshing виджета значение false.

1
srLayout.isRefreshing = false

Если вы запустите приложение сейчас, удалите несколько элементов списка и выполните жест «пролистывание до обновления», вы увидите сам сброс списка.

Material Design существует уже несколько лет, и большинство пользователей в наши дни ожидают, что вы справитесь со многими жестами, которые он упоминает. К счастью, это не требует особых усилий. В этом уроке вы узнали, как реализовать два очень распространенных жеста смахивания. Вы также узнали, как постепенно открывать вид, скрытый за предметом списка.

Вы можете узнать больше о жестах и ​​движении материала, обратившись к руководству по дизайну жестов.