diff --git a/base-filters/.gitignore b/base-filters/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/base-filters/.gitignore @@ -0,0 +1 @@ +/build diff --git a/base-filters/README.md b/base-filters/README.md new file mode 100644 index 0000000..27c7be0 --- /dev/null +++ b/base-filters/README.md @@ -0,0 +1,89 @@ +# Описание + +Модуль содержит реализацию следующих типов фильтров: + +1. Выбор одного из доступных значений списка +2. Выбор нескольких доступных значений из списка +3. добавить остальные по ходу реализаации + +# Использование + +## Выбор одного/нескольких из доступных значений списка + +### Как использовать +``` kotlin +val selectorView = ListSelectionView>(context) + .Builder() + .setItems(navArgs.items) + .addItemDecoration((TopDividerItemDecoration( + context = requireContext(), + drawableId = R.drawable.list_divider_1dp, + startMargin = START_MARGIN_DIVIDER_DP.px + ))) + .withSelectionType(ListSelectionView.SelectionType.SINGLE_SELECT) + .onResultListener { items -> + viewModel.dispatchAction(SelectItemAction.SelectItem(items)) + } + .build() +``` +### Конфигурации +* при создании `ListSelectionView` необходимо передлать `ItemType` - класс модели данных в списке, `HolderType` - класс viewHolder-а в recyclerView. +Для использования дефолтной реализации необходимо использовать типы `>` +* в метод `setItems(List)` необходимо передать список объектов +* метод `addItemDecoration(itemDecoration: RecyclerView.ItemDecoration)` можно использовать для передачи объекта `RecyclerView.ItemDecoration` +* метод `withSelectionType(type: SelectionType)` используется для указания типа выбора: + * `SINGLE_SELECT` - по умолчанию - позволяет выбрать один выариант, при этом будет выбран всегда как минимум один вариант + * `MULTI_SELECT` - позволяет выбрать несколько вариантов из списка, при этом можно полностью выбрать все варианты и убрать выделение со всех вариантов +* метод `showInHolder(HolderFactoryType)` используется для определения кастомного viewHolder для списка с недефолтной разметкой. +``` kotlin +val selectorView = ListSelectionView(context) + .Builder() + .showInHolder { parent, clickListener, selectionType -> + TestItemViewHolder( + binding = TestSelectionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + onItemSelectAction = clickListener, + selectionType = selectionType + ) + } + ... + .build() +``` +* колбэк `onSelectedItemsListener(listener: OnSelectedItemsListener)` можно использовать для получения списка всех элементов `ItemType` после каждого выбора +* колбэк `onSelectedItemListener(listener: OnSelectedItemListener)` можно использовать для получения элемента списка `ItemType`, по которому произошел клик +* после вызова конфигурационных методов обязательно необходимо вызать метод `build()` + +### Кастомизация стиля дефолтной реализации ViewHolder без необходимости создания кастомного layout и viewHolder + +#### 1. Определить кастомную тему и стили элементов +1. Стиль для **текста элемента списка** должен быть наследником стиля `Widget.FilterSelection.Item` +``` xml + +``` +2. Стиль для **индикатора выбора** должен быть наследником стиля `Widget.FilterSelection.Radio` +Передайте `selector-drawable` для кастомизации вида индикатора в конце строки +``` xml + +``` +3. Создайте **тему**, которая должна быть наследником `Theme.FilterSelection` +``` xml + +``` +#### 2. Применить тему при создании view +При создании вью в коде можно указать тему, используя `ContextThemeWrapper` +``` kotlin +val newContext = ContextThemeWrapper(requireContext(), R.style.Theme_Custom_FilterSelection) +val selectorView = ListSelectionView(newContext) + .Builder() + ... + .build() +``` diff --git a/base-filters/build.gradle b/base-filters/build.gradle new file mode 100644 index 0000000..6c57558 --- /dev/null +++ b/base-filters/build.gradle @@ -0,0 +1,41 @@ +apply from: "../android-configs/lib-config.gradle" +apply plugin: 'kotlin-parcelize' + +android { + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation project(":utils") + implementation project(":recyclerview-adapters") + implementation project(":navigation-base") + implementation project(":kotlin-extensions") + + implementation("org.jetbrains.kotlin:kotlin-stdlib") + + implementation("androidx.core:core-ktx") + + implementation("androidx.appcompat:appcompat") + implementation("com.google.android.material:material") + implementation("androidx.constraintlayout:constraintlayout") { + version { + require '2.0.0' + } + } + + constraints { + implementation("androidx.appcompat:appcompat") { + version { + require '1.0.0' + } + } + + implementation("androidx.core:core-ktx") { + version { + require '1.0.0' + } + } + } +} diff --git a/base-filters/src/main/AndroidManifest.xml b/base-filters/src/main/AndroidManifest.xml new file mode 100644 index 0000000..14e5b35 --- /dev/null +++ b/base-filters/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/ListSelectionView.kt b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/ListSelectionView.kt new file mode 100644 index 0000000..c19db8e --- /dev/null +++ b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/ListSelectionView.kt @@ -0,0 +1,136 @@ +package ru.touchin.roboswag.base_filters.select_list_item + +import android.content.Context +import android.util.AttributeSet +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import androidx.annotation.StyleRes +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import ru.touchin.roboswag.base_filters.databinding.SelectionItemBinding +import ru.touchin.roboswag.base_filters.select_list_item.adapter.BaseSelectionViewHolder +import ru.touchin.roboswag.base_filters.select_list_item.adapter.HolderFactoryType +import ru.touchin.roboswag.base_filters.select_list_item.adapter.SelectionItemViewHolder +import ru.touchin.roboswag.base_filters.select_list_item.adapter.SheetSelectionAdapter +import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem + +private typealias OnSelectedItemListener = (item: ItemType) -> Unit +private typealias OnSelectedItemsListener = (items: List) -> Unit + +/** + * Base [ListSelectionView] to use in filters screen for choosing single or multi items in list. + * + * @param ItemType Type of model's element in list. + * It must implement [BaseSelectionItem] abstract class. + * + * @param HolderType Type of viewHolder in recyclerView. + * It must extend [BaseSelectionViewHolder] abstract class. + * + **/ + +class ListSelectionView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RecyclerView(context, attrs, defStyleAttr) + where ItemType : BaseSelectionItem, + HolderType : BaseSelectionViewHolder { + + enum class SelectionType { SINGLE_SELECT, MULTI_SELECT } + + constructor(context: Context, @StyleRes themeResId: Int) : this(ContextThemeWrapper(context, themeResId)) + + private var mutableItems: List = emptyList() + private var selectionType = SelectionType.SINGLE_SELECT + + private var onSelectedItemChanged: OnSelectedItemListener? = null + private var onSelectedItemsChanged: OnSelectedItemsListener? = null + private var factory: HolderFactoryType = getDefaultFactory() + + init { + layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + layoutManager = LinearLayoutManager(context) + } + + private fun getDefaultFactory(): HolderFactoryType = { parent, clickListener, selectionType -> + SelectionItemViewHolder( + binding = SelectionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + onItemSelectAction = clickListener, + selectionType = selectionType + ) + } + + private val selectionAdapter by lazy { + SheetSelectionAdapter( + onItemSelectAction = onItemSelectedListener, + selectionType = selectionType, + factory = factory + ) + } + + private val onItemSelectedListener: (item: ItemType) -> Unit = { item -> + onSelectedItemChanged?.invoke(item) + updateAfterSelection(item) + onSelectedItemsChanged?.invoke(mutableItems) + } + + fun updateItems(items: List) { + mutableItems = items + updateList() + } + + private fun updateList() { + selectionAdapter.submitList(mutableItems) + } + + private fun updateAfterSelection(selectedItem: ItemType) { + mutableItems = mutableItems.map { item -> + when { + item.isItemTheSame(selectedItem) -> selectedItem + selectionType == SelectionType.SINGLE_SELECT -> item.copyWithSelection(isSelected = false) + else -> item + } + } + updateList() + } + + inner class Builder { + + fun setItems(items: List) = apply { + mutableItems = items + } + + fun setItems( + source: List, + mapper: (T) -> ItemType + ) = setItems(source.map { item -> mapper.invoke(item) }) + + fun showInHolder(holderFactory: HolderFactoryType) = apply { + factory = holderFactory + } + + fun addItemDecoration(itemDecoration: RecyclerView.ItemDecoration) = apply { + this@ListSelectionView.addItemDecoration(itemDecoration) + } + + fun onSelectedItemListener(listener: OnSelectedItemListener) = apply { + this@ListSelectionView.onSelectedItemChanged = listener + } + + fun onSelectedItemsListener(listener: OnSelectedItemsListener) = apply { + this@ListSelectionView.onSelectedItemsChanged = listener + } + + fun withSelectionType(type: SelectionType) = apply { + selectionType = type + } + + fun build() = this@ListSelectionView.also { + it.adapter = selectionAdapter + updateList() + } + } +} diff --git a/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/BaseSelectionViewHolder.kt b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/BaseSelectionViewHolder.kt new file mode 100644 index 0000000..847bfc2 --- /dev/null +++ b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/BaseSelectionViewHolder.kt @@ -0,0 +1,11 @@ +package ru.touchin.roboswag.base_filters.select_list_item.adapter + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem + +abstract class BaseSelectionViewHolder(val view: View) + : RecyclerView.ViewHolder(view) { + + abstract fun bind(item: ItemType) +} diff --git a/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/SelectionItemViewHolder.kt b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/SelectionItemViewHolder.kt new file mode 100644 index 0000000..0fc9a88 --- /dev/null +++ b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/SelectionItemViewHolder.kt @@ -0,0 +1,33 @@ +package ru.touchin.roboswag.base_filters.select_list_item.adapter + +import android.view.View +import ru.touchin.roboswag.base_filters.databinding.SelectionItemBinding +import ru.touchin.roboswag.base_filters.select_list_item.ListSelectionView +import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem + +class SelectionItemViewHolder( + private val binding: SelectionItemBinding, + private val onItemSelectAction: (ItemType) -> Unit, + private val selectionType: ListSelectionView.SelectionType +) : BaseSelectionViewHolder(binding.root) { + + override fun bind(item: ItemType) { + binding.itemTitle.text = item.title + binding.itemRadiobutton.isChecked = item.isSelected + + setupCheckListener(item) + } + + private fun setupCheckListener(item: ItemType) = with(binding) { + val checkListener = View.OnClickListener { + itemRadiobutton.isChecked = true + onItemSelectAction.invoke(item.copyWithSelection(isSelected = when (selectionType) { + ListSelectionView.SelectionType.SINGLE_SELECT -> true + else -> !item.isSelected + })) + } + root.setOnClickListener(checkListener) + itemRadiobutton.setOnClickListener(checkListener) + } + +} diff --git a/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/SheetSelectionAdapter.kt b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/SheetSelectionAdapter.kt new file mode 100644 index 0000000..b8d6449 --- /dev/null +++ b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/SheetSelectionAdapter.kt @@ -0,0 +1,30 @@ +package ru.touchin.roboswag.base_filters.select_list_item.adapter + +import androidx.recyclerview.widget.DiffUtil +import ru.touchin.roboswag.base_filters.select_list_item.ListSelectionView +import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem +import ru.touchin.roboswag.recyclerview_adapters.adapters.DelegationListAdapter + +class SheetSelectionAdapter( + onItemSelectAction: (ItemType) -> Unit, + selectionType: ListSelectionView.SelectionType, + factory: HolderFactoryType +) : DelegationListAdapter(object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: BaseSelectionItem, newItem: BaseSelectionItem): Boolean = + oldItem.isItemTheSame(newItem) + + override fun areContentsTheSame(oldItem: BaseSelectionItem, newItem: BaseSelectionItem): Boolean = + oldItem.isContentTheSame(newItem) + +}) { + + init { + addDelegate(SheetSelectionDelegate( + onItemSelectAction = onItemSelectAction, + selectionType = selectionType, + factory = factory + )) + } + +} diff --git a/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/SheetSelectionDelegate.kt b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/SheetSelectionDelegate.kt new file mode 100644 index 0000000..14f5460 --- /dev/null +++ b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/adapter/SheetSelectionDelegate.kt @@ -0,0 +1,28 @@ +package ru.touchin.roboswag.base_filters.select_list_item.adapter + +import android.view.ViewGroup +import ru.touchin.roboswag.base_filters.select_list_item.ListSelectionView.SelectionType +import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem +import ru.touchin.roboswag.recyclerview_adapters.adapters.ItemAdapterDelegate + +typealias HolderFactoryType = (ViewGroup, (ItemType) -> Unit, SelectionType) -> BaseSelectionViewHolder + +class SheetSelectionDelegate( + private val onItemSelectAction: (ItemType) -> Unit, + private val selectionType: SelectionType, + private val factory: HolderFactoryType +) : ItemAdapterDelegate, ItemType>() + where ItemType : BaseSelectionItem { + + override fun onCreateViewHolder(parent: ViewGroup): BaseSelectionViewHolder = + factory.invoke(parent, onItemSelectAction, selectionType) + + override fun onBindViewHolder( + holder: BaseSelectionViewHolder, + item: ItemType, + adapterPosition: Int, + collectionPosition: Int, + payloads: MutableList + ) = holder.bind(item) + +} diff --git a/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/model/BaseSelectionItem.kt b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/model/BaseSelectionItem.kt new file mode 100644 index 0000000..8f59990 --- /dev/null +++ b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/model/BaseSelectionItem.kt @@ -0,0 +1,14 @@ +package ru.touchin.roboswag.base_filters.select_list_item.model + +abstract class BaseSelectionItem( + open val id: Int, + open val title: String, + open val isSelected: Boolean +) { + + abstract fun isItemTheSame(compareItem: BaseSelectionItem): Boolean + + abstract fun isContentTheSame(compareItem: BaseSelectionItem): Boolean + + abstract fun copyWithSelection(isSelected: Boolean): ItemType +} diff --git a/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/model/DefaultSelectionItem.kt b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/model/DefaultSelectionItem.kt new file mode 100644 index 0000000..603961f --- /dev/null +++ b/base-filters/src/main/java/ru/touchin/roboswag/base_filters/select_list_item/model/DefaultSelectionItem.kt @@ -0,0 +1,24 @@ +package ru.touchin.roboswag.base_filters.select_list_item.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class DefaultSelectionItem( + override val id: Int, + override val title: String, + override val isSelected: Boolean = false +) : BaseSelectionItem(id, title, isSelected), Parcelable { + + override fun isItemTheSame(compareItem: BaseSelectionItem): Boolean = when { + compareItem is DefaultSelectionItem && id == compareItem.id -> true + else -> false + } + + override fun isContentTheSame(compareItem: BaseSelectionItem): Boolean = + this == compareItem + + @Suppress("UNCHECKED_CAST") + override fun copyWithSelection(isSelected: Boolean): ItemType = + this.copy(isSelected = isSelected) as ItemType +} diff --git a/base-filters/src/main/res/layout/selection_item.xml b/base-filters/src/main/res/layout/selection_item.xml new file mode 100644 index 0000000..72bb7eb --- /dev/null +++ b/base-filters/src/main/res/layout/selection_item.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/base-filters/src/main/res/values/attrs.xml b/base-filters/src/main/res/values/attrs.xml new file mode 100644 index 0000000..bd6618a --- /dev/null +++ b/base-filters/src/main/res/values/attrs.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/base-filters/src/main/res/values/styles.xml b/base-filters/src/main/res/values/styles.xml new file mode 100644 index 0000000..380b4da --- /dev/null +++ b/base-filters/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + + + +