Skip to content

Instantly share code, notes, and snippets.

@milhauscz
Last active April 21, 2021 13:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milhauscz/25c46c629779f83426330a041a601969 to your computer and use it in GitHub Desktop.
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
/**
* 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