Статьи

Как добавить множественный выбор в Android RecyclerView

Виджет RecyclerView является неотъемлемой частью большинства приложений Android сегодня. С тех пор как он был добавлен в библиотеку поддержки Android в конце 2014 года, он затмил виджет ListView как наиболее предпочтительный виджет для отображения больших и сложных списков. Однако в нем отсутствует одна важная функция: поддержка выбора и отслеживания элементов списка. RecyclerView Selection , аддонная библиотека Google, выпущенная в марте этого года, пытается это исправить.

В этом уроке я покажу вам, как использовать новую библиотеку для создания приложения, которое предлагает интуитивно понятный интерфейс для выбора нескольких элементов в списке. Следуйте этому примеру Android RecyclerView множественного выбора, и вы узнаете некоторые навыки, которые можно применять в своих собственных приложениях.

Чтобы следовать, вам нужно:

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

Чтобы добавить библиотеку RecyclerView Selection в проект Android Studio, укажите следующие зависимости implementation в файле build.gradle модуля app :

1
2
implementation ‘com.android.support:recyclerview-v7:28.0.0’
implementation ‘com.android.support:recyclerview-selection:28.0.0’

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

Чтобы сохранить данные каждого элемента списка, создайте класс данных Kotlin с именем Person и добавьте к нему два свойства: name и phone .

1
2
data class Person(val name:String,
                 val phone: String)

Теперь вы можете продолжить и создать список объектов Person в вашей основной деятельности.

1
2
3
4
5
6
7
8
val myList = listOf(
    Person(«Alice», «555-0111»),
    Person(«Bob», «555-0119»),
    Person(«Carol», «555-0141»),
    Person(«Dan», «555-0155»),
    Person(«Eric», «555-0180»),
    Person(«Craig», «555-0145»)
)

Мы, конечно, будем использовать виджет RecyclerView для отображения списка. Поэтому добавьте <RecyclerView> в XML-файл макета вашей основной деятельности.

1
2
3
4
5
6
<android.support.v7.widget.RecyclerView
        android:layout_width=»match_parent»
        android:layout_height=»match_parent»
        android:id=»@+id/my_rv»>
 
</android.support.v7.widget.RecyclerView>

Чтобы указать расположение элементов списка, создайте новый файл XML и назовите его list_item.xml . Внутри него добавьте два виджета TextView : один для отображения имени, а другой для отображения номера телефона. Если вы используете элемент LinearLayout для позиционирования виджетов, содержимое файла XML должно выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<LinearLayout
    xmlns:android=»https://schemas.android.com/apk/res/android»
    android:orientation=»vertical» android:layout_width=»match_parent»
    android:layout_height=»wrap_content»
    android:padding=»16dp»>
 
    <TextView
        android:layout_width=»match_parent»
        android:layout_height=»wrap_content»
        android:id=»@+id/list_item_name»
        style=»@style/TextAppearance.AppCompat.Large»/>
 
    <TextView
        android:layout_width=»match_parent»
        android:layout_height=»wrap_content»
        android:id=»@+id/list_item_phone»
        style=»@style/TextAppearance.AppCompat.Small»/>
         
</LinearLayout>

Вы можете думать о держателе представления как об объекте, который содержит ссылки на представления, представленные в макете элементов списка. Без этого виджет RecyclerView не сможет эффективно отображать элементы списка.

На данный момент вам нужен держатель представления, который содержит два виджета TextView вы создали на предыдущем шаге. Поэтому создайте новый класс, который расширяет класс RecyclerView.ViewHolder , и инициализируйте ссылки на оба виджета внутри него. Вот как:

1
2
3
4
5
6
7
8
9
class MyViewHolder(view: View)
    : RecyclerView.ViewHolder(view) {
 
    val name: TextView = view.list_item_name
    val phone: TextView = view.list_item_phone
 
    // More code here
     
}

Кроме того, для дополнения RecyclerView Selection требуется метод, который он может вызывать для уникальной идентификации выбранных элементов списка. Этот метод в идеале принадлежит самому владельцу вида. Кроме того, он должен возвращать экземпляр класса ItemDetailsLookup.ItemDetails . Поэтому добавьте следующий код в держатель представления:

1
2
3
4
5
6
fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
       object: ItemDetailsLookup.ItemDetails<Long>() {
 
           // More code here
 
       }

Теперь вы должны переопределить два абстрактных метода, присутствующих в классе ItemDetails . Начните с переопределения getPosition() и возврата свойства adapterPosition держателя представления внутри него. Свойство adapterPosition обычно представляет собой не что иное, как индекс элемента списка.

1
override fun getPosition(): Int = adapterPosition

Затем переопределите метод getSelectionKey() . Этот метод должен возвращать ключ, который можно использовать для уникальной идентификации элемента списка. Для простоты давайте просто itemId свойство itemId держателя представления.

1
override fun getSelectionKey(): Long?

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

Чтобы дополнение RecyclerView Selection работало корректно, каждый раз, когда пользователь касается виджета RecyclerView , необходимо преобразовать координаты касания в объект ItemDetails .

Создайте новый класс, который расширяет класс ItemDetailsLookup , и добавьте в него конструктор, который может принимать виджет RecyclerView в качестве аргумента. Обратите внимание, что, поскольку класс является абстрактным, Android Studio автоматически сгенерирует заглушку для своего абстрактного метода.

1
2
3
4
5
6
7
8
9
class MyLookup(private val rv: RecyclerView)
    : ItemDetailsLookup<String>() {
    override fun getItemDetails(event: MotionEvent)
                                : ItemDetails<String>?
 
        // More code here
         
    }
}

Как видно из приведенного выше кода, метод getItemDetails() получает объект MotionEvent . findChildViewUnder() координаты X и Y события в метод findChildViewUnder() , вы можете определить представление, связанное с элементом списка, к findChildViewUnder() прикоснулся пользователь. Чтобы преобразовать объект View объект ItemDetails , все, что вам нужно сделать, это вызвать метод getItemDetails() . Вот как:

1
2
3
4
5
6
val view = rv.findChildViewUnder(event.x, event.y)
if(view != null) {
    return (rv.getChildViewHolder(view) as MyViewHolder)
            .getItemDetails()
}
return null

Теперь вам понадобится адаптер, который может связать ваш список с вашим виджетом RecyclerView . Чтобы создать его, создайте новый класс, который расширяет класс RecyclerView.Adapter . Поскольку адаптеру необходим доступ к списку и контексту вашей деятельности, новый класс должен иметь конструктор, который может принимать оба аргумента в качестве аргументов.

1
2
3
4
5
class MyAdapter(private val listItems:List<Person>,
                private val context: Context)
    : RecyclerView.Adapter<MyViewHolder>() {
 
}

Важно, чтобы вы явно указали, что каждый элемент этого адаптера будет иметь уникальный стабильный идентификатор типа Long . Лучшее место для этого — внутри блока init .

1
2
3
init {
    setHasStableIds(true)
}

Кроме того, чтобы иметь возможность использовать позицию элемента в качестве его уникального идентификатора, вам необходимо переопределить метод getItemId() .

1
2
3
override fun getItemId(position: Int): Long {
    return position.toLong()
}

Поскольку класс RecyclerView.Adapter является абстрактным, теперь вам придется переопределить еще три метода, чтобы сделать ваш адаптер пригодным для использования.

Сначала переопределите метод getItemCount() чтобы вернуть размер списка.

1
override fun getItemCount(): Int = listItems.size

Затем переопределите метод onCreateViewHolder() . Этот метод должен возвращать экземпляр класса держателя представления, который вы создали ранее в этом руководстве. Чтобы создать такой экземпляр, вы должны вызвать конструктор класса и передать ему раздутый макет элементов списка. Чтобы надуть макет, используйте метод LayoutInflater класса LayoutInflater . Вот как:

1
2
3
4
5
6
override fun onCreateViewHolder(parent: ViewGroup,
                               viewType: Int): MyViewHolder =
       MyViewHolder(
           LayoutInflater.from(context)
               .inflate(R.layout.list_item, parent, false)
       )

Наконец, переопределите метод onBindViewHolder() и соответствующим образом инициализируйте свойство text обоих виджетов TextView присутствующих в держателе представления.

1
2
3
4
override fun onBindViewHolder(vh: MyViewHolder, position: Int) {
    vh.name.text = listItems[position].name
    vh.phone.text = listItems[position].phone
}

На данный момент, у вас есть почти все, что вам нужно, чтобы сделать свой список Однако вы все равно должны указать, как вы хотите, чтобы элементы списка были расположены. А пока давайте расположим их один под другим, используя экземпляр LinearLayoutManager .

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

Добавьте следующий код к вашей основной деятельности:

1
2
my_rv.layoutManager = LinearLayoutManager(this)
my_rv.setHasFixedSize(true)

Наконец, назначьте новый экземпляр вашего adapter свойству adapter виджета RecyclerView .

1
2
val adapter = MyAdapter(myList, this)
my_rv.adapter = adapter

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

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

Виджет RecyclerView прежнему не позволяет выбирать какие-либо элементы. Чтобы включить выбор из нескольких элементов, вам понадобится объект SelectionTracker в вашей деятельности.

1
private var tracker: SelectionTracker<Long>?

Вы можете инициализировать трекер, используя класс SelectionTracker.Builder . Его конструктору необходимо передать идентификатор выбора, ваш виджет RecyclerView , ключевой поставщик, класс поиска сведений о вашем элементе и стратегию хранения.

Вы можете использовать любую строку в качестве идентификатора выбора. В качестве ключевого поставщика вы можете использовать экземпляр класса StableIdKeyProvider .

Библиотека RecyclerView Selection предлагает множество стратегий хранения, каждая из которых гарантирует, что выбранные элементы не отменяются при повороте устройства пользователя или когда система Android закрывает ваше приложение во время перерыва в ресурсах. На данный момент, поскольку тип ваших клавиш выбора — Long , вы должны использовать объект StorageStrategy типа Long .

Когда Builder будет готов, вы можете вызвать его withSelectionPredicate() чтобы указать, сколько элементов вы хотите разрешить пользователю выбирать. Для поддержки выбора нескольких элементов в качестве аргумента метода необходимо передать объект SelectionPredicate возвращаемый методом createSelectAnything() .

Соответственно, добавьте следующий код в метод onCreate() вашей деятельности:

1
2
3
4
5
6
7
8
9
tracker = SelectionTracker.Builder<Long>(
               «selection-1»,
               my_rv,
               StableIdKeyProvider(my_rv),
               MyLookup(my_rv),
               StorageStrategy.createLongStorage()
         ).withSelectionPredicate(
               SelectionPredicates.createSelectAnything()
         ).build()

Чтобы максимально использовать стратегию хранения, вы всегда должны пытаться восстановить состояние трекера внутри onCreate() .

1
2
if(savedInstanceState != null)
       tracker?.onRestoreInstanceState(savedInstanceState)

Точно так же вы должны убедиться, что вы сохранили состояние трекера в onSaveInstanceState() вашей активности.

1
2
3
4
5
6
override fun onSaveInstanceState(outState: Bundle?) {
    super.onSaveInstanceState(outState)
 
    if(outState != null)
        tracker?.onSaveInstanceState(outState)
}

Трекер выбора не очень полезен, если он не связан с вашим адаптером. Поэтому передайте его адаптеру, вызвав метод setTracker() .

1
adapter.setTracker(tracker)

Метод setTracker() еще не существует, поэтому добавьте следующий код в свой класс адаптера:

1
2
3
4
5
private var tracker: SelectionTracker<Long>?
 
fun setTracker(tracker: SelectionTracker<Long>?) {
    this.tracker = tracker
}

Если вы попытаетесь запустить свое приложение на этом этапе, вы сможете выбрать элементы в списке. Когда вы входите в режим выбора из нескольких элементов при длительном нажатии на элемент списка, вы сможете почувствовать краткую вибрацию на большинстве устройств. Однако, поскольку выбранные элементы в настоящее время неотличимы от невыбранных, у вас не будет визуальной обратной связи. Чтобы это исправить, вам нужно внести несколько изменений в метод onBindViewHolder() вашего адаптера.

Обычный способ выделить выбранные элементы — изменить цвет фона. Поэтому теперь вы должны изменить цвет LinearLayout виджета LinearLayout который присутствует в XML-файле макета ваших элементов. Чтобы получить ссылку на него, получите ссылку на родителя одного из виджетов TextView доступных в держателе представления.

Добавьте следующий код непосредственно перед концом метода onBindViewHolder() :

1
2
3
val parent = vh.name.parent as LinearLayout
 
// More code here

Затем вы можете вызвать метод isSelected() объекта SelectionTracker чтобы определить, выбран элемент или нет.

Следующий код показывает, как изменить цвет фона выделенных элементов на голубой:

1
2
3
4
5
6
7
8
if(tracker!!.isSelected(position.toLong())) {
    parent.background = ColorDrawable(
            Color.parseColor(«#80deea»)
    )
} else {
    // Reset color to white if not selected
    parent.background = ColorDrawable(Color.WHITE)
}

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

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

Обычно вы хотите показать пользователю, сколько элементов выбрано в данный момент. С библиотекой RecyclerView Selection это сделать очень просто.

addObserver() объект SelectionObserver с трекером выбора, вызвав метод addObserver() . Внутри onSelectionChanged() наблюдателя вы можете обнаружить изменения в количестве выбранных элементов.

1
2
3
4
5
6
7
8
tracker?.addObserver(
        object: SelectionTracker.SelectionObserver<Long>() {
            override fun onSelectionChanged() {
                val nItems:Int?
         
                // More code here
            }
})

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
if(nItems!=null && nItems > 0) {
 
    // Change title and color of action bar
 
    title = «$nItems items selected»
    supportActionBar?.setBackgroundDrawable(
            ColorDrawable(Color.parseColor(«#ef6c00»)))
} else {
 
    // Reset color and title to default values
 
    title = «RVSelection»
    supportActionBar?.setBackgroundDrawable(
            ColorDrawable(getColor(R.color.colorPrimary)))
}

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

Приложение отображает количество выбранных предметов

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

Чтобы узнать больше о библиотеке, обратитесь к официальной документации .