Skip to content

Instantly share code, notes, and snippets.

@Aidanvii7
Created December 18, 2021 17:58
Show Gist options
  • Save Aidanvii7/47cb3091a36154217b3014569ad3743d to your computer and use it in GitHub Desktop.
Save Aidanvii7/47cb3091a36154217b3014569ad3743d to your computer and use it in GitHub Desktop.
Binding adapter for Spinner
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)
}
}
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