Last active
April 21, 2021 13:48
-
-
Save milhauscz/25c46c629779f83426330a041a601969 to your computer and use it in GitHub Desktop.
Custom implementation of an Android spinner (combo box) with material theme and data binding support by extending AutocompleteTextView
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
/** | |
* Custom implementation of an Android spinner (combo box) with material theme by using AutocompleteTextView. | |
* Recommended to use together with [MaterialSpinnerAdapter]. | |
* | |
* Supports listening to and updating the selected position e. g. from a LiveData object bound via | |
* the `selectedPosition` two-way binding adapter. The empty text value can be bound via 'emptyText' | |
* attr (custom styleable must be declared in attrs.xml). | |
* | |
* If [MaterialSpinnerAdapter] is used, its [MaterialSpinnerAdapter.selectedItemPosition] is | |
* automatically updated when selection changes. | |
* | |
* Created by Milos Cernilovsky on 12/9/20. | |
* @see MaterialSpinnerAdapter | |
*/ | |
class MaterialSpinner : AppCompatAutoCompleteTextView { | |
/** | |
* These listeners will receive updates when the selected position changes. Can be used e. g. | |
* by adapters to update the selected item's UI. | |
*/ | |
private val selectedPositionListeners = mutableSetOf<(Int) -> Unit>() | |
private var _selectedPosition: Int = 0 | |
set(value) { | |
field = value | |
notifyListenersAboutSelectedPosition() | |
} | |
private fun notifyListenersAboutSelectedPosition() { | |
selectedPositionListeners.forEach { | |
it.invoke(_selectedPosition) | |
} | |
} | |
/** | |
* Currently selected position in the spinner | |
*/ | |
var selectedPosition: Int | |
get() { | |
return _selectedPosition | |
} | |
set(value) { | |
_selectedPosition = value | |
setCurrentPositionItemText() | |
} | |
/** | |
* Empty text will be shown if there are no items in the adapter or if the [selectedPosition] | |
* does not exist in the adapter. | |
*/ | |
var emptyText: CharSequence? = null | |
set(value) { | |
field = value | |
setCurrentPositionItemText() | |
} | |
private var wrappedItemClickListener: AdapterView.OnItemClickListener? = null | |
init { | |
super.setOnItemClickListener { parent, view, position, id -> | |
_selectedPosition = position | |
wrappedItemClickListener?.onItemClick(parent, view, position, id) | |
} | |
inputType = InputType.TYPE_NULL | |
} | |
constructor(context: Context) : this(context, null) | |
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.attr.autoCompleteTextViewStyle) | |
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { | |
val a = context.obtainStyledAttributes(attrs, R.styleable.MaterialSpinner, defStyleAttr, 0) | |
emptyText = a.getText(R.styleable.MaterialSpinner_emptyText) | |
a.recycle() | |
} | |
override fun setOnItemClickListener(l: AdapterView.OnItemClickListener?) { | |
wrappedItemClickListener = l | |
} | |
override fun <T> setAdapter(adapter: T) where T : ListAdapter?, T : Filterable? { | |
super.setAdapter(adapter) | |
setCurrentPositionItemText() | |
if (adapter is MaterialSpinnerAdapter<*>) { | |
addSelectedPositionListener(adapter::selectedItemPosition::set) | |
} | |
} | |
fun addSelectedPositionListener(listener: (Int) -> Unit) { | |
selectedPositionListeners.add(listener) | |
listener.invoke(_selectedPosition) | |
} | |
fun removeSelectedPositionListener(listener: (Int) -> Unit) { | |
selectedPositionListeners.remove(listener) | |
} | |
private fun setCurrentPositionItemText() { | |
adapter?.let { adapter -> | |
setText(if (adapter.count > selectedPosition) { | |
adapter.getItem(selectedPosition)?.toString() ?: emptyText | |
} else { | |
emptyText | |
}, false) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment