Skip to content

Commit e3c50d2

Browse files
authored
feature(plugin): added data selector utility (#308)
1 parent b84a1b0 commit e3c50d2

15 files changed

+519
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.pluto.utilities.selector
2+
3+
import androidx.activity.ComponentActivity
4+
import androidx.activity.viewModels
5+
import androidx.fragment.app.Fragment
6+
import androidx.fragment.app.activityViewModels
7+
import androidx.lifecycle.LiveData
8+
import androidx.lifecycle.MediatorLiveData
9+
import androidx.lifecycle.ViewModel
10+
import com.pluto.utilities.SingleLiveEvent
11+
import com.pluto.utilities.list.ListItem
12+
13+
class Selector : ViewModel() {
14+
15+
internal val singleChoiceData: LiveData<SelectorData<SelectorOption>>
16+
get() = _singleChoiceData
17+
private val _singleChoiceData = SingleLiveEvent<SelectorData<SelectorOption>>()
18+
internal val singleChoiceResult = SingleLiveEvent<SelectorOption>()
19+
20+
internal val multiChoiceData: LiveData<SelectorData<List<SelectorOption>>>
21+
get() = _multiChoiceData
22+
private val _multiChoiceData = SingleLiveEvent<SelectorData<List<SelectorOption>>>()
23+
internal val multiChoiceResult = SingleLiveEvent<List<SelectorOption>>()
24+
25+
val data = MediatorLiveData<Any>()
26+
27+
init {
28+
data.addSource(singleChoiceData) { data.postValue(it) }
29+
data.addSource(multiChoiceData) { data.postValue(it) }
30+
}
31+
32+
fun selectSingle(title: String, list: List<SelectorOption>, preSelected: SelectorOption? = null): SingleLiveEvent<SelectorOption> {
33+
_singleChoiceData.postValue(SelectorData(title, list, preSelected))
34+
return singleChoiceResult
35+
}
36+
37+
fun selectMultiple(title: String, list: List<SelectorOption>, preSelected: List<SelectorOption>? = null): SingleLiveEvent<List<SelectorOption>> {
38+
_multiChoiceData.postValue(SelectorData(title, list, preSelected))
39+
return multiChoiceResult
40+
}
41+
}
42+
43+
abstract class SelectorOption : ListItem() {
44+
abstract fun displayText(): String
45+
}
46+
47+
internal data class SelectorData<T>(val title: String, val list: List<SelectorOption>, val preSelected: T?)
48+
49+
fun Fragment.lazyDataSelector(): Lazy<Selector> = activityViewModels()
50+
51+
fun ComponentActivity.lazyDataSelector(): Lazy<Selector> = viewModels()
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.pluto.utilities.selector.ui
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.View.GONE
7+
import android.view.ViewGroup
8+
import com.google.android.material.bottomsheet.BottomSheetBehavior
9+
import com.google.android.material.bottomsheet.BottomSheetDialog
10+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
11+
import com.pluto.plugin.R
12+
import com.pluto.plugin.databinding.PlutoSelectorDialogBinding
13+
import com.pluto.utilities.SingleLiveEvent
14+
import com.pluto.utilities.device.Device
15+
import com.pluto.utilities.extensions.dp
16+
import com.pluto.utilities.list.CustomItemDecorator
17+
import com.pluto.utilities.list.DiffAwareAdapter
18+
import com.pluto.utilities.list.DiffAwareHolder
19+
import com.pluto.utilities.list.ListItem
20+
import com.pluto.utilities.selector.SelectorData
21+
import com.pluto.utilities.selector.SelectorOption
22+
import com.pluto.utilities.selector.lazyDataSelector
23+
import com.pluto.utilities.setOnDebounceClickListener
24+
import com.pluto.utilities.viewBinding
25+
26+
class DataSelectorDialog : BottomSheetDialogFragment() {
27+
28+
private val binding by viewBinding(PlutoSelectorDialogBinding::bind)
29+
private val selector by lazyDataSelector()
30+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
31+
inflater.inflate(R.layout.pluto___selector_dialog, container, false)
32+
33+
override fun getTheme(): Int = R.style.PlutoBottomSheetDialogTheme
34+
35+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
36+
super.onViewCreated(view, savedInstanceState)
37+
dialog?.setOnShowListener {
38+
val dialog = it as BottomSheetDialog
39+
val bottomSheet = dialog.findViewById<View>(R.id.design_bottom_sheet)
40+
bottomSheet?.let {
41+
dialog.behavior.peekHeight = Device(requireContext()).screen.heightPx
42+
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
43+
}
44+
}
45+
selector.singleChoiceData.observe(viewLifecycleOwner) {
46+
binding.title.text = it.title
47+
setupSingleChoiceUI(it)
48+
}
49+
selector.multiChoiceData.observe(viewLifecycleOwner) {
50+
binding.title.text = it.title
51+
setupMultiChoiceUI(it)
52+
}
53+
}
54+
55+
private fun setupSingleChoiceUI(data: SelectorData<SelectorOption>) {
56+
binding.doneCta.visibility = GONE
57+
binding.clear.visibility = GONE
58+
binding.list.apply {
59+
adapter = SingleSelectorAdapter(
60+
object : DiffAwareAdapter.OnActionListener {
61+
override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
62+
if (data is SelectorOption) {
63+
selector.singleChoiceResult.postValue(data)
64+
dismiss()
65+
}
66+
}
67+
},
68+
data.preSelected
69+
).apply { this.list = data.list }
70+
addItemDecoration(CustomItemDecorator(requireContext(), DECORATOR_DIVIDER_PADDING))
71+
}
72+
}
73+
74+
private fun setupMultiChoiceUI(data: SelectorData<List<SelectorOption>>) {
75+
val tempSelectedOptionLiveData = SingleLiveEvent<List<SelectorOption>>()
76+
data.preSelected.let { tempSelectedOptionLiveData.postValue(it) }
77+
78+
binding.list.apply {
79+
adapter = MultiSelectorAdapter(
80+
object : DiffAwareAdapter.OnActionListener {
81+
override fun onAction(action: String, data: ListItem, holder: DiffAwareHolder) {
82+
if (data is SelectorOption) {
83+
val tempSet = tempSelectedOptionLiveData.value?.toHashSet() ?: hashSetOf()
84+
if (!tempSet.add(data)) {
85+
tempSet.remove(data)
86+
}
87+
tempSelectedOptionLiveData.postValue(tempSet.toList())
88+
}
89+
}
90+
},
91+
tempSelectedOptionLiveData
92+
).apply { this.list = data.list }
93+
addItemDecoration(CustomItemDecorator(requireContext(), DECORATOR_DIVIDER_PADDING))
94+
}
95+
binding.doneCta.setOnDebounceClickListener {
96+
selector.multiChoiceResult.postValue(tempSelectedOptionLiveData.value?.toList() ?: emptyList())
97+
dismiss()
98+
}
99+
100+
binding.clear.setOnDebounceClickListener {
101+
selector.multiChoiceResult.postValue(emptyList())
102+
dismiss()
103+
}
104+
}
105+
106+
private companion object {
107+
val DECORATOR_DIVIDER_PADDING = 16f.dp.toInt()
108+
}
109+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.pluto.utilities.selector.ui
2+
3+
import android.view.ViewGroup
4+
import androidx.lifecycle.Observer
5+
import com.pluto.plugin.R
6+
import com.pluto.plugin.databinding.PlutoMultiChoiceSelectorItemBinding
7+
import com.pluto.utilities.SingleLiveEvent
8+
import com.pluto.utilities.extensions.inflate
9+
import com.pluto.utilities.list.BaseAdapter
10+
import com.pluto.utilities.list.DiffAwareAdapter
11+
import com.pluto.utilities.list.DiffAwareHolder
12+
import com.pluto.utilities.list.ListItem
13+
import com.pluto.utilities.selector.SelectorOption
14+
import com.pluto.utilities.setOnDebounceClickListener
15+
import com.pluto.utilities.spannable.setSpan
16+
17+
internal class MultiSelectorAdapter(
18+
private val listener: OnActionListener,
19+
private val selectedLiveData: SingleLiveEvent<List<SelectorOption>>
20+
) : BaseAdapter() {
21+
22+
override fun getItemViewType(item: ListItem): Int = 1
23+
24+
override fun onViewHolderCreated(parent: ViewGroup, viewType: Int) = MultiSelectorItemHolder(parent, listener, selectedLiveData)
25+
}
26+
27+
internal class MultiSelectorItemHolder(
28+
parent: ViewGroup,
29+
listener: DiffAwareAdapter.OnActionListener,
30+
private val selectedLiveData: SingleLiveEvent<List<SelectorOption>>
31+
) : DiffAwareHolder(parent.inflate(R.layout.pluto___multi_choice_selector_item), listener) {
32+
33+
private val binding = PlutoMultiChoiceSelectorItemBinding.bind(itemView)
34+
35+
override fun onBind(item: ListItem) {
36+
if (item is SelectorOption) {
37+
binding.title.setSpan {
38+
append(item.displayText())
39+
}
40+
41+
binding.root.setOnDebounceClickListener {
42+
onAction("click")
43+
}
44+
}
45+
}
46+
47+
override fun onAttachViewHolder() {
48+
super.onAttachViewHolder()
49+
selectedLiveData.observeForever(selectedChoicesObserver)
50+
}
51+
52+
override fun onDetachViewHolder() {
53+
super.onDetachViewHolder()
54+
selectedLiveData.removeObserver(selectedChoicesObserver)
55+
}
56+
57+
private val selectedChoicesObserver = Observer<List<SelectorOption>> {
58+
binding.checkbox.isSelected = it?.contains(item) ?: run { false }
59+
}
60+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.pluto.utilities.selector.ui
2+
3+
import android.view.ViewGroup
4+
import com.pluto.plugin.R
5+
import com.pluto.plugin.databinding.PlutoSingleChoiceSelectorItemBinding
6+
import com.pluto.utilities.extensions.inflate
7+
import com.pluto.utilities.list.BaseAdapter
8+
import com.pluto.utilities.list.DiffAwareAdapter
9+
import com.pluto.utilities.list.DiffAwareHolder
10+
import com.pluto.utilities.list.ListItem
11+
import com.pluto.utilities.selector.SelectorOption
12+
import com.pluto.utilities.setOnDebounceClickListener
13+
import com.pluto.utilities.spannable.setSpan
14+
15+
internal class SingleSelectorAdapter(private val listener: OnActionListener, private val selected: SelectorOption?) : BaseAdapter() {
16+
17+
override fun getItemViewType(item: ListItem): Int = 1
18+
19+
override fun onViewHolderCreated(parent: ViewGroup, viewType: Int): DiffAwareHolder = SingleSelectorItemHolder(parent, listener, selected)
20+
}
21+
22+
internal class SingleSelectorItemHolder(
23+
parent: ViewGroup,
24+
listener: DiffAwareAdapter.OnActionListener,
25+
private val selected: SelectorOption?
26+
) : DiffAwareHolder(parent.inflate(R.layout.pluto___single_choice_selector_item), listener) {
27+
28+
private val binding = PlutoSingleChoiceSelectorItemBinding.bind(itemView)
29+
30+
override fun onBind(item: ListItem) {
31+
if (item is SelectorOption) {
32+
binding.title.setSpan {
33+
append(item.displayText())
34+
}
35+
binding.radio.isSelected = selected?.let { it == item } ?: run { false }
36+
binding.root.setOnDebounceClickListener {
37+
onAction("click")
38+
}
39+
}
40+
}
41+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
3+
<item android:drawable="@drawable/pluto___ic_check_box_selected" android:state_selected="true" />
4+
<item android:drawable="@drawable/pluto___ic_check_box_unselected" />
5+
</selector>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="@color/pluto___blue"
8+
android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
9+
</vector>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="@color/pluto___dark_40"
8+
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
9+
</vector>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
3+
<item android:drawable="@drawable/pluto___ic_radio_selected" android:state_selected="true" />
4+
<item android:drawable="@drawable/pluto___ic_radio_unselected" />
5+
</selector>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="@color/pluto___blue"
8+
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
9+
</vector>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="@color/pluto___dark_40"
8+
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
9+
</vector>

0 commit comments

Comments
 (0)