[Разработка под Android, Kotlin] Выбор элементов recylerView при помощи dataBinding'а

Автор Сообщение
news_bot ®

Стаж: 6 лет 4 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
03-Дек-2020 17:30

Всем привет. На днях столкнулся с проблемой реализации выбора нескольких элементов в RecyclerView с использованием dataBinding'а.Сразу за делоДля начала напишем базовый адаптер, поддерживающий dataBinding.
/**
* Универсальный адаптер для data binding'а.
* @param layoutRes id layout'а, который будет установлен для итемов
* @param lifecycleOwner lifecycle owner фрагмента или активти, в котором лежит recycler view
* @param itemBindingId id переменной в layout'е, в это поле устанавливается итем
* @param onClick метод, вызываемый при клике на итем
*/
class RecyclerViewAdapter<Item : IRecyclerViewItem>(
    @LayoutRes private val layoutRes: Int,
    private val lifecycleOwner: LifecycleOwner,
    private val itemBindingId: Int? = null,
    private val onClick: ((Item) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerViewAdapter<Item>.ViewHolder>() {
    private val items = mutableListOf<Item>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        //Создаем базовый ViewDataBinding экземпляр с переданным layoutRes
        val binding = DataBindingUtil.inflate<ViewDataBinding>(inflater, layoutRes, parent, false)
        return ViewHolder(binding)
    }
    override fun getItemCount(): Int = items.size
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        //По сути просто вызываем onBind с текущим итемом
        val item = items[position]
        holder.onBind(item)
    }
    /**
     * Установка итемов в адаптер
     *
     * @param newItems новыйе итемы
     */
    fun setItems(newItems: List<Item>) {
        val diffUtilCallback = DiffUtilCallback(newItems)
        val diffResult = DiffUtil.calculateDiff(diffUtilCallback)
        items.apply {
            clear()
            addAll(newItems)
        }
        diffResult.dispatchUpdatesTo(this)
    }
    //Тут происходит вся магия DataBinding'а
    inner class ViewHolder(
        private val binding: ViewDataBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        fun onBind(item: Item) {
            binding.apply {
                //Установка переменных
                setVariable(itemBindingId ?: BR.item, item)
                root.setOnClickListener { onClick?.invoke(item) }
                lifecycleOwner = this@RecyclerViewAdapter.lifecycleOwner
            }
        }
    }
    private inner class DiffUtilCallback(private val newItems: List<Item>) : DiffUtil.Callback() {
        override fun getOldListSize(): Int = itemCount
        override fun getNewListSize(): Int = newItems.size
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return newItems[newItemPosition].id == items[oldItemPosition].id
        }
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return newItems[newItemPosition] == items[oldItemPosition]
        }
    }
}
Так же для работы DiffUtil я сделал интерфейс, который показывает, что элемент имеет уникальное поле
/**
* Интерфейс для ui моделей, необходим для RecyclerViewAdapter.
* @property id уникальное поле итема
*/
interface IRecyclerViewItem {
    val id: Int
}
До недавнего времени, данный адаптер позволял решить почти все задачи со списками. В зависимости от проекта onClick можно заменить на onBind: (binding: ViewDataBinding) -> Unit, тем самым можно сделать настройку отдельных элементов итема.Selection helperНастало время магии, пора писать сам SelectionHelper, который будет работать для dataBinding'а и иметь высокую производительность.
За время написания возможности выбора элементов из списка, была создана куча костылей, которые обладали очень медленным перфомансом при поиске выбранных элементов, или же код реализации был кривым.Вот самый лучший, на мой взгляд, вариант:
class SelectionHelper<T : IRecyclerViewItem> : ISelectionHelper<T>() {
    //Мапа со всеми выбранными элементами
    private val selectedItems = mutableMapOf<Int, T>()
    //Обработка итема, если он уже выбран - убираем его, иначе - наоборот
    override fun handleItem(item: T) {
        if (selectedItems[item.id] == null) {
            selectedItems[item.id] = item
        } else {
            selectedItems.remove(item.id)
        }
        //Уведомляем dataBining, что пора бы обновить ui)
        notifyChange()
    }
    override fun isSelected(id: Int): Boolean = selectedItems.containsKey(id)
    override fun getSelectedItems(): List<T> = selectedItems.values.toList()
    override fun getSelectedItemsSize(): Int = selectedItems.size
}
// Наследуем класс от BaseObservable, для того, что бы dataBinding мог следить за
// изменением сотояния хелпера
abstract class ISelectionHelper<T : IRecyclerViewItem> : BaseObservable() {
    abstract fun handleItem(item: T)
    abstract fun isSelected(id: Int): Boolean
    abstract fun getSelectedItems(): List<T>
    abstract fun getSelectedItemsSize(): Int
}
Из преимуществ такого подхода можно выделить:
  • Со стороны viewModel мы можем иметь быстрый доступ к выбранным элементам через selectionHelper.getSelectedItems, при надобности.
  • Возможность использовать DataBinding, без надобности как-то уведомлять adapter о изменении состояния итема
  • Выделение можно делать как под копотом адаптера, так и настраивать все через тот же самый onBind
Теперь для работы с таким хелпером нам надо:
  • Создать сам хелпер в viewModel/presenter или где угодно, где он нужен
  • Передать его в адаптер
  • Модифицировать xml итема
С первым пунктом не должно быть каких-либо проблем, а вот вторым мы сейчас и займемсяПереписываем adadpter
class RecyclerViewAdapter<Item : IRecyclerViewItem>(
    @LayoutRes private val layoutRes: Int,
    private val lifecycleOwner: LifecycleOwner,
    private val itemBindingId: Int? = null,
    //Делаем его нулабельным, что бы не поломать логику, когда нам не нужно выделение
    private val selectionHelper: ISelectionHelper<Item>? = null,
    private val onClick: ((Item) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerViewAdapter<Item>.ViewHolder>() {
  ...
      inner class ViewHolder(
        private val binding: ViewDataBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        fun onBind(item: Item) {
            binding.apply {
                //Установка переменных
                setVariable(itemBindingId ?: BR.item, item)
                selectionHelper?.let { setVariable(BR.selectionHelper, it) }
                root.setOnClickListener {
                    //Вызываем обработку элемента
                    selectionHelper?.handleItem(item)
                    onClick?.invoke(item)
                }
                lifecycleOwner = this@RecyclerViewAdapter.lifecycleOwner
            }
        }
    }
}
Сейчас при каждом клике элемент будет менять свое состояние выбран/не выбран, это поведение можно поменять, сделав метод onBind, или же как-либо по другому.Весь код адаптера
class RecyclerViewAdapter<Item : IRecyclerViewItem>(
    @LayoutRes private val layoutRes: Int,
    private val lifecycleOwner: LifecycleOwner,
    private val itemBindingId: Int? = null,
    private val selectionHelper: ISelectionHelper<Item>? = null,
    private val onClick: ((Item) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerViewAdapter<Item>.ViewHolder>() {
    private val items = mutableListOf<Item>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding = DataBindingUtil.inflate<ViewDataBinding>(inflater, layoutRes, parent, false)
        return ViewHolder(binding)
    }
    override fun getItemCount(): Int = items.size
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = items[position]
        holder.onBind(item)
    }
    /**
     * Установка итемов в адаптер
     *
     * @param newItems новыйе итемы
     */
    fun setItems(newItems: List<Item>) {
        val diffUtilCallback = DiffUtilCallback(newItems)
        val diffResult = DiffUtil.calculateDiff(diffUtilCallback)
        items.apply {
            clear()
            addAll(newItems)
        }
        diffResult.dispatchUpdatesTo(this)
    }
    inner class ViewHolder(
        private val binding: ViewDataBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        fun onBind(item: Item) {
            binding.apply {
                //Установка переменных
                setVariable(itemBindingId ?: BR.item, item)
                selectionHelper?.let { setVariable(BR.selectionHelper, it) }
                root.setOnClickListener {
                    //Вызываем обработку элемента
                    selectionHelper?.handleItem(item)
                    onClick?.invoke(item)
                }
                lifecycleOwner = this@RecyclerViewAdapter.lifecycleOwner
            }
        }
    }
    private inner class DiffUtilCallback(private val newItems: List<Item>) : DiffUtil.Callback() {
        override fun getOldListSize(): Int = itemCount
        override fun getNewListSize(): Int = newItems.size
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return newItems[newItemPosition].id == items[oldItemPosition].id
        }
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return newItems[newItemPosition] == items[oldItemPosition]
        }
    }
}
Модифицируем наш итемИ так, пришло время немного переписать xml итема, добавляем наш selectionHelper как пременную в xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="item"
            type="ImageItemUi" />
        <!-- А вот и он) -->
        <variable
            name="selectionHelper"
            type="dev.syncended.ctime.utils.ui.ISelectionHelper<ImageItemUi>" />
        <import type="dev.syncended.ctime.models.ui.ImageItemUi" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_margin="@dimen/ui_spacing_normal"
            android:padding="@dimen/1dp"
            android:scaleType="centerCrop"
            app:file="@{item.file}"
            app:item_id="@{item.id}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintDimensionRatio="1:1"
            app:layout_constraintTop_toTopOf="parent"
            app:selection_helper="@{selectionHelper}"
            tools:ignore="ContentDescription" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Сейчас для выделения я сделал padding = 1dp, для того, что бы менять фон выделенного изображения, по факту отображение выделения зависит только от вашей фантазии.Добавляем новый bindingAdapter для обработки изменений в selectionHelper
//Обработка изменений selectionHelper'а, для этого нам нужен id итема
@BindingAdapter("selection_helper", "item_id", requireAll = true)
fun <T : IRecyclerViewItem> handleSelection(
    view: View,
    selectionHelper: ISelectionHelper<T>,
    itemId: Int
) {
    //Смотрим текущее состояние итема
    val isSelected = selectionHelper.isSelected(itemId)
    //Выбираем цвет в зависимости от состояния
    val color = if (isSelected) {
        R.color.color_primary
    } else {
        android.R.color.transparent
    }
    view.setBackgroundColor(ContextCompat.getColor(view.context, color))
}
Таким вот образом, если элемент выбран мы меняем ему background.РезультатВот список элементов до клика по ним:
После кликов получаем вот такой результат:
ЗаключениеПо результату мы получили довольно простой инструмент для выделения элементов списка. Если отойти от обычного выделения рамкой, можно будет менять состояние, допустим, чекбокса, в зависимости от того, выбран элемент или нет.
android:checked=@{selectionHelper.isSelected(item.id)}
По аналогии можно сделать кучу разных вариаций использования данного хэлепера.Спасибо за прочтение, это моя первая статья, так что не судите строго, а так же держите котика.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_android, #_kotlin, #_recyclerview, #_databinding, #_algorithms, #_razrabotka_pod_android (
Разработка под Android
)
, #_kotlin
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 24-Июн 00:34
Часовой пояс: UTC + 5