[Разработка под Android, Kotlin] Выбор элементов recylerView при помощи dataBinding'а
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет. На днях столкнулся с проблемой реализации выбора нескольких элементов в 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
===========
Похожие новости:
- [Браузеры] Vivaldi 3.5 для Android — Аккуратная работа
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android] Как ВТБ помогает снизить комиссию за приём платежей до 0,4% с помощью QR-кода
- [Разработка под Android, DevOps] Прокачиваем Android проект с GitHub Actions. Часть 1
- [Программирование, Разработка под Android] Рисование собственных представлений (View) в Android (перевод)
- [Open source, Яндекс API, Kotlin, Телемедицина, Ember.js] Как мы начали социальный Open Source проект Brain-Up по созданию платформы развития слухового восприятия
- [Программирование, Разработка под Android, Kotlin] Меняем стандартный диалог сбоя приложения в Android на собственный экран (перевод)
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android] Стажировка для мобильных разработчиков в Redmadrobot
- [Разработка мобильных приложений, Разработка под Android, Смартфоны] Работа с камерой на платформе HMS: улучшаем качество съёмки и добавляем различные режимы в свои приложения
- [Разработка мобильных приложений, Разработка под Android, Разработка под Windows, Софт] Microsoft разрабатывает возможность запуска Android-приложений на Windows 10
- Microsoft развивает прослойку для запуска Android-приложений в Windows
Теги для поиска: #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_android, #_kotlin, #_recyclerview, #_databinding, #_algorithms, #_razrabotka_pod_android (
Разработка под Android
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:09
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет. На днях столкнулся с проблемой реализации выбора нескольких элементов в 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] } } } /**
* Интерфейс для ui моделей, необходим для RecyclerViewAdapter. * @property id уникальное поле итема */ interface IRecyclerViewItem { val id: Int } За время написания возможности выбора элементов из списка, была создана куча костылей, которые обладали очень медленным перфомансом при поиске выбранных элементов, или же код реализации был кривым.Вот самый лучший, на мой взгляд, вариант: 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 }
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 } } } } 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 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> //Обработка изменений 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)) } После кликов получаем вот такой результат: ЗаключениеПо результату мы получили довольно простой инструмент для выделения элементов списка. Если отойти от обычного выделения рамкой, можно будет менять состояние, допустим, чекбокса, в зависимости от того, выбран элемент или нет. android:checked=@{selectionHelper.isSelected(item.id)}
=========== Источник: habr.com =========== Похожие новости:
Разработка под Android ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:09
Часовой пояс: UTC + 5