My app has an AutoCompleteTextView that queries a Room database and displays the search results in the dropdown as the user types. The results are displayed correctly but the dropdown's height is always too large for the number of items (see screenshot below).
Basically the AutoCompleteTextView has a TextWatcher that sends the current text to the ViewModel, which in turn queries the database. Once the results are received, the ViewModel updates the view state (a Flow) and the fragment, which observes it, reassigns the AutoCompleteTextView's adapter with the new data.
Dropdown item layout
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/spacing_8">
<GridLayout
android:id="@+id/imageContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
... />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/txtName"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
... />
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment layout
<com.google.android.material.textfield.TextInputLayout>
...
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/autoCompleteTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/spacing_16"/>
</com.google.android.material.textfield.TextInputLayout>
Adapter
internal class StationAdapter(
context: Context,
private val stations: List<UiStation>
) : ArrayAdapter<String>(
context,
R.layout.item_station,
stations.map { it.name }
) {
fun getStation(position: Int): UiStation = stations[position]
override fun getCount(): Int = stations.size
override fun getItem(position: Int): String = stations[position].name
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return convertView ?: run {
val inflater = LayoutInflater.from(context)
val binding = ItemStationBinding.inflate(inflater, parent, false).apply {
val station = stations[position]
imageContainer.addIconsForModes(station.modes)
txtName.text = station.name
}
binding.root
}
}
...
}
Fragment
private fun handleState(state: StationSearchViewState) = with(state) {
...
stations?.let {
val adapter = StationAdapter(
context = requireContext(),
stations = it
)
binding.autoCompleteTextView.setAdapter(adapter)
}
}
Now these are the other things I tried as well but didn't work (got the same result):
notifyDataSetChanged()
android:dropDownHeight
property to wrap_content
The only thing that seems to work is when I use an ArrayAdapter
with the android.R.layout.simple_list_item_1
layout for the items
After 3 days working hard to figure this out I finally managed to find a solution, which is partly in this answer to an unrelated question.
Here is my code:
private const val DROPDOWN_ITEM_MAX_COUNT = 5
private const val PADDING = 34
class CustomAutoCompleteTextView(
context: Context,
attributeSet: AttributeSet?
) : MaterialAutoCompleteTextView(context, attributeSet) {
override fun onFilterComplete(count: Int) {
val itemCount = if (count > DROPDOWN_ITEM_MAX_COUNT) {
DROPDOWN_ITEM_MAX_COUNT
} else {
count
}
val individualItemHeight = (height / 2) + PADDING
dropDownHeight = itemCount * individualItemHeight
super.onFilterComplete(count)
}
}
Essentially I had to create a custom AutoCompleteTextView
which, once the data is filtered, takes an item count (no greater than the maximum I've set) and multiplies it by the height of an individual item on my dropdown (a) plus a padding (b) to make sure the dropdown view always wraps the items.
a: I reached this value with a bit of trial and error but it still works across different screen sizes and densities
b: not mandatory. I just thought adding a bit of padding made it look nicer
End result