androidlayoutdropdownautocompletetextview

AutoCompleteTextView dropdown height not wrapping to content


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).

Dropdown with results

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):

  1. Passing all of the database table's results to the adapter and implementing a custom filter
  2. Implementing a function to submit new data to the adapter and calling notifyDataSetChanged()
  3. Setting the AutoCompleteTextView's 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


Solution

  • 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

    Single result

    Multiple results