Created
December 18, 2021 17:58
-
-
Save Aidanvii7/47cb3091a36154217b3014569ad3743d to your computer and use it in GitHub Desktop.
Binding adapter for Spinner
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.view.View | |
import androidx.annotation.IdRes | |
import androidx.databinding.BindingAdapter | |
import androidx.databinding.adapters.ListenerUtil as FrameworkListenerUtil | |
/** | |
* Designed to track objects passed as optional parameters via static [BindingAdapter] methods. | |
* | |
* Only one instance per instanceResId can be tracked at a time. | |
* | |
* This is useful for *addListener* and *removeListener* methods, | |
* where associated [BindingAdapter] methods must replace the previously added listener, or remove it. | |
* | |
* It is a wrapper around [FrameworkListenerUtil.trackListener], with less specific naming, as the instance being tracked does not | |
* necessarily need to be a listener. | |
* | |
* Instances are tracked by referential equality rather than structural equality - that is, | |
* a new instance with the same structural equality but different referential equality will trigger an [onDetached] > [onAttached] cycle. | |
* | |
* Example usage: | |
* ```kotlin | |
* @BindingAdapter("textWatcher") | |
* fun TextView.setTextWatcher(textWatcher: TextWatcher?) { | |
* trackInstance( | |
* newInstance = textWatcher, | |
* instanceResId = R.id.textWatcher, | |
* onAttached = { | |
* // [it] is the newly added listener, called when non-null. | |
* addTextChangedListener(it) | |
* }, | |
* onDetached = { | |
* // [it] is the previously added listener, called when non-null. | |
* removeTextChangedListener(it) | |
* } | |
* ) | |
* } | |
* ``` | |
* | |
* For tracking values based on structural equality, use [trackValue] instead. | |
*/ | |
inline fun <V : View, I> V.trackInstance( | |
newInstance: I?, | |
@IdRes instanceResId: Int, | |
onDetached: V.(I) -> Unit = {}, | |
onAttached: V.(I) -> Unit = {}, | |
) { | |
val oldInstance = FrameworkListenerUtil.trackListener(this, newInstance, instanceResId) | |
if (oldInstance !== newInstance) { | |
oldInstance?.let { onDetached(oldInstance) } | |
newInstance?.let { onAttached(newInstance) } | |
} | |
} | |
/** | |
* Like [trackInstance], though tracks objects based on structural equality. | |
*/ | |
inline fun <V : View, I> V.trackValue( | |
newValue: I?, | |
@IdRes valueResId: Int, | |
onNewValue: V.(I) -> Unit = {}, | |
onOldValue: V.(I) -> Unit = {}, | |
) { | |
val oldValue = FrameworkListenerUtil.trackListener(this, newValue, valueResId) | |
if (oldValue != newValue) { | |
oldValue?.let { onOldValue(oldValue) } | |
newValue?.let { onNewValue(newValue) } | |
} | |
} | |
fun <Value> View.getTracked(@IdRes valueResId: Int): Value? = | |
FrameworkListenerUtil.getListener<Value>(this, valueResId) | |
fun <Value> View.setTracked( | |
@IdRes valueResId: Int, | |
value: Value, | |
) { | |
FrameworkListenerUtil.trackListener(this, value, valueResId) | |
} | |
inline fun <Value> View.onTracked( | |
@IdRes instanceResId: Int, | |
onNextValue: (Value) -> Unit, | |
) { | |
onTracked<Value>( | |
newValue = null, | |
instanceResId = instanceResId, | |
onNextValue = onNextValue, | |
) | |
} | |
inline fun <Value> View.onTracked( | |
newValue: Value?, | |
@IdRes instanceResId: Int, | |
onNextValue: (Value) -> Unit, | |
) { | |
val oldValue = FrameworkListenerUtil.trackListener(this, newValue, instanceResId) | |
when { | |
newValue == null -> { | |
FrameworkListenerUtil.trackListener(this, oldValue, instanceResId) | |
if (oldValue != newValue) { | |
onNextValue(oldValue) | |
} | |
} | |
oldValue != newValue -> onNextValue(newValue) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.view.View | |
import android.widget.AdapterView | |
import android.widget.ArrayAdapter | |
import android.widget.Spinner | |
import androidx.databinding.BindingAdapter | |
import androidx.databinding.InverseBindingAdapter | |
import androidx.databinding.InverseBindingListener | |
import com.enocgo.driverapp.R | |
import java.util.concurrent.atomic.AtomicBoolean | |
@Suppress("UNCHECKED_CAST") | |
@InverseBindingAdapter(attribute = "selectedItem", event = "selectedItemAttrChanged") | |
internal fun <T : Any> Spinner.getBoundSelectedItem(): T? = | |
items?.getOrNull(selectedItemPosition)?.let { selectedItem -> | |
if (selectedItem is UnselectedItem) null else selectedItem as T | |
} | |
private val Spinner.items: MutableList<Any>? | |
get() = getTracked<MutableList<Any>>(R.id.list_items) | |
internal fun interface GetUnselectedItemText { | |
operator fun invoke(itemDisplayString: CharSequence): CharSequence | |
} | |
@BindingAdapter( | |
"selectedItem", | |
"selectedItemAttrChanged", | |
"items", | |
"unselectedItemText", | |
requireAll = false | |
) | |
internal fun <T : Any> Spinner.bind( | |
selectedItem: T?, | |
selectedItemAttrChanged: InverseBindingListener?, | |
items: List<T>?, | |
unselectedItemText: CharSequence?, | |
) { | |
bind( | |
selectedItem = selectedItem, | |
selectedItemAttrChanged = selectedItemAttrChanged, | |
items = items, | |
getUnselectedItemText = unselectedItemText?.let { { unselectedItemText } }, | |
) | |
} | |
@BindingAdapter( | |
"selectedItem", | |
"selectedItemAttrChanged", | |
"items", | |
"getUnselectedItemText", | |
requireAll = false | |
) | |
internal fun <T : Any> Spinner.bind( | |
selectedItem: T?, | |
selectedItemAttrChanged: InverseBindingListener?, | |
items: List<T>?, | |
getUnselectedItemText: GetUnselectedItemText?, | |
) { | |
if (items != null) { | |
trackValue( | |
newValue = items.toMutableList<Any>(), | |
valueResId = R.id.list_items, | |
onNewValue = { newItems -> | |
val previouslySelectedItem: Any? = this.selectedItem | |
getOrCreateArrayAdapter(selectedItemAttrChanged).apply { | |
setNotifyOnChange(false) | |
clear() | |
addAll(*newItems.toTypedArray()) | |
notifyDataSetChanged() | |
if (previouslySelectedItem != null) { | |
val newIndexOfPreviouslySelectedItem = newItems.indexOf(previouslySelectedItem) | |
if (newIndexOfPreviouslySelectedItem != -1) { | |
setSelection(newIndexOfPreviouslySelectedItem) | |
} else if (getUnselectedItemText != null) { | |
val unselectedItem = buildUnselectedItem { | |
getUnselectedItemText(previouslySelectedItem.toString()) | |
} | |
newItems.add(0, unselectedItem) | |
insert(unselectedItem, 0) | |
setSelection(0) | |
selectedItemAttrChanged?.onChange() | |
} | |
} | |
} | |
}, | |
) | |
if (selectedItem != null && selectedItem != this.selectedItem) { | |
this.items?.indexOf(selectedItem)?.let { itemIndex -> | |
if (itemIndex >= 0) { | |
setSelection(itemIndex) | |
} | |
} | |
} | |
} else releaseArrayAdapterAndItems() | |
} | |
private inline fun buildUnselectedItem(getUnselectedItemText: () -> CharSequence): UnselectedItem = | |
getUnselectedItemText().run { | |
if (isEmpty()) UnselectedItem.Empty else UnselectedItem.WithText(toString()) | |
} | |
private fun Spinner.getOrCreateArrayAdapter(selectedItemAttrChanged: InverseBindingListener?): ArrayAdapter<Any> = | |
arrayAdapter ?: createArrayAdapter(selectedItemAttrChanged) | |
private val Spinner.arrayAdapter: ArrayAdapter<Any>? | |
get() = getTracked<ArrayAdapter<Any>>(R.id.array_adapter) | |
private fun Spinner.createArrayAdapter(selectedItemAttrChanged: InverseBindingListener?): ArrayAdapter<Any> = | |
ArrayAdapter<Any>(context, R.layout.spinner_item).also { arrayAdapter -> | |
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) | |
onItemSelectedListener = OnItemSelectedListener( | |
spinner = this, | |
arrayAdapter = arrayAdapter, | |
selectedItemAttrChanged = selectedItemAttrChanged, | |
) | |
trackInstance( | |
newInstance = arrayAdapter, | |
instanceResId = R.id.array_adapter, | |
onAttached = { newArrayAdapter -> | |
adapter = newArrayAdapter | |
}, | |
) | |
} | |
private sealed class UnselectedItem { | |
object Empty : UnselectedItem() { | |
override fun toString() = "" | |
} | |
data class WithText( | |
val displayText: String, | |
) : UnselectedItem() { | |
override fun toString() = displayText | |
} | |
} | |
private class OnItemSelectedListener( | |
private val spinner: Spinner, | |
private val arrayAdapter: ArrayAdapter<Any>, | |
private val selectedItemAttrChanged: InverseBindingListener? | |
) : AdapterView.OnItemSelectedListener { | |
private val firstRun = AtomicBoolean(true) | |
override fun onItemSelected( | |
parent: AdapterView<*>?, | |
view: View?, | |
position: Int, | |
id: Long, | |
) { | |
if (firstRun.getAndSet(false)) { | |
return | |
} | |
if (position != 0) { | |
val firstItem = spinner.getItemAtPosition(0) | |
if (firstItem is UnselectedItem) { | |
arrayAdapter.remove(firstItem) | |
spinner.items?.removeAt(0) | |
spinner.setSelection(position - 1) | |
} | |
} | |
selectedItemAttrChanged?.onChange() | |
} | |
override fun onNothingSelected(parent: AdapterView<*>?) { | |
selectedItemAttrChanged?.onChange() | |
} | |
} | |
private fun Spinner.releaseArrayAdapterAndItems() { | |
adapter = null | |
trackInstance<Spinner, ArrayAdapter<Any>>( | |
newInstance = null, | |
instanceResId = R.id.array_adapter, | |
onDetached = { | |
onItemSelectedListener = null | |
}, | |
) | |
trackValue<Spinner, List<Any>>( | |
newValue = null, | |
valueResId = R.id.list_items, | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment