Proper ArrayAdapter implementation for an AutoCompleteTextView that doesn't just search the start of the String

53 views Asked by At

The Default ArrayAdapter only checks, if the possible suggestions starts with the current input (it uses .startsWith. It doesn't use .contains. However I would like it to use .contains.

I already know about these two threads:

However, these don't really do what I want them to do. All of them have at least one of these problems:

1. The ArrayAdapter Class extends ThemedSpinnerAdapter which requires API level 23

Downloading the sourcecode of the ArrayAdapter class and changing stuff in there, would be limited to API level 23 and up, because I can't find the ArrayAdapter for older versions.

2. When the list object gets updated from outside the adapter after the constructor has been called, the suggestions don't get updated

The best possible way I see to work around this is, overriding the notifyDataSetChanged function. In that function I would then just copy the current Items in the list from the constructor into the temporary list that contains all the items that should be filtered through.
However, the notifyDataSetChanged function also gets called from the Filter.publishResults function, thereby causing the 3rd problem.

3. anything that was part of the list but didn't get suggested, will never appear again

To change what is shown, the list from the constructor has to be changed. However, this change has to be somehow undoable. The best way I know to do this would be to add a second list, that contains all the items from the first one, but the list object itself is a different one. But then this list has to be kept up to date somehow (That's the 2nd problem).

An implementation that suffers from the 3rd problem:

import android.content.Context
import android.widget.ArrayAdapter
import android.widget.Filter
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes

class CustomStringArrayAdapter(
    context: Context,
    @LayoutRes resource: Int,
    @IdRes textViewResourceId: Int = 0,
    private val objects: List<String> = ArrayList()
) : ArrayAdapter<String>(context, resource, textViewResourceId, objects) {
    
    inner class CustomStringFilter : Filter() {
        
        override fun performFiltering(constraint: CharSequence?): FilterResults {
            val filterResults = FilterResults()
            
            if (!constraint.isNullOrEmpty()) {
                val suggestions = mutableListOf<String>()
                for (element in objects) {
                    if (element.contains(constraint, true)) {
                        suggestions.add(element)
                    }
                }
                
                filterResults.values = suggestions
                filterResults.count = suggestions.size
            }
            
            return filterResults
        }
        
        override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
            if (results == null || results.count <= 0) {
                notifyDataSetInvalidated()
                return
            }
            
            val resultsValues = results.values
            if (resultsValues !is List<*>) {
                notifyDataSetInvalidated()
                return
            }
            
            val result = mutableListOf<String>()
            for (resultValue in resultsValues) {
                if (resultValue is String) {
                    result.add(resultValue)
                }
            }
            
            if (result.isEmpty()) {
                notifyDataSetInvalidated()
            } else {
                clear()
                addAll(result)
                notifyDataSetChanged()
            }
        }
    }
    
    private lateinit var mFilter: Filter
    
    override fun getFilter(): Filter {
        if (!::mFilter.isInitialized) {
            mFilter = CustomStringFilter()
        }
        return mFilter
    }
    
}

An implementation that suffers from the 2nd problem:

import android.content.Context
import android.widget.ArrayAdapter
import android.widget.Filter
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes

class CustomStringArrayAdapter(
    context: Context,
    @LayoutRes resource: Int,
    @IdRes textViewResourceId: Int = 0,
    private val objects: List<String> = ArrayList()
) : ArrayAdapter<String>(context, resource, textViewResourceId, objects) {
    
    inner class CustomStringFilter : Filter() {
        
        override fun performFiltering(constraint: CharSequence?): FilterResults {
            val filterResults = FilterResults()
            
            if (!constraint.isNullOrEmpty()) {
                val completeList = tempObjects.toList()
                filterResults.values = completeList
                filterResults.count = completeList.size
            } else {
                val suggestions = mutableListOf<String>()
                for (element in tempObjects) {
                    if (element.contains(constraint, true)) {
                        suggestions.add(element)
                    }
                }
                
                filterResults.values = suggestions
                filterResults.count = suggestions.size
            }
            
            return filterResults
        }
        
        override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
            if (results == null || results.count <= 0) {
                notifyDataSetInvalidated()
                return
            }
            
            val resultsValues = results.values
            if (resultsValues !is List<*>) {
                notifyDataSetInvalidated()
                return
            }
            
            val result = mutableListOf<String>()
            for (resultValue in resultsValues) {
                if (resultValue is String) {
                    result.add(resultValue)
                }
            }
            
            if (result.isEmpty()) {
                notifyDataSetInvalidated()
            } else {
                clear()
                addAll(result)
                notifyDataSetChanged()
            }
        }
    }
    
    private lateinit var mFilter: Filter
    private val tempObjects = objects.toMutableList()
    
    override fun getFilter(): Filter {
        if (!::mFilter.isInitialized) {
            mFilter = CustomStringFilter()
        }
        return mFilter
    }
    
}

And another implementation that suffers from the 3rd problem:

import android.content.Context
import android.widget.ArrayAdapter
import android.widget.Filter
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes

class CustomStringArrayAdapter(
    context: Context,
    @LayoutRes resource: Int,
    @IdRes textViewResourceId: Int = 0,
    private val objects: List<String> = ArrayList()
) : ArrayAdapter<String>(context, resource, textViewResourceId, objects) {
    
    inner class CustomStringFilter : Filter() {
        
        override fun performFiltering(constraint: CharSequence?): FilterResults {
            val filterResults = FilterResults()
            
            if (!constraint.isNullOrEmpty()) {
                val completeList = tempObjects.toList()
                filterResults.values = completeList
                filterResults.count = completeList.size
            } else {
                val suggestions = mutableListOf<String>()
                for (element in tempObjects) {
                    if (element.contains(constraint, true)) {
                        suggestions.add(element)
                    }
                }
                
                filterResults.values = suggestions
                filterResults.count = suggestions.size
            }
            
            return filterResults
        }
        
        override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
            if (results == null || results.count <= 0) {
                notifyDataSetInvalidated()
                return
            }
            
            val resultsValues = results.values
            if (resultsValues !is List<*>) {
                notifyDataSetInvalidated()
                return
            }
            
            val result = mutableListOf<String>()
            for (resultValue in resultsValues) {
                if (resultValue is String) {
                    result.add(resultValue)
                }
            }
            
            if (result.isEmpty()) {
                notifyDataSetInvalidated()
            } else {
                clear()
                addAll(result)
                notifyDataSetChanged()
            }
        }
    }
    
    private lateinit var mFilter: Filter
    private val tempObjects = objects.toMutableList()
    
    override fun notifyDataSetChanged() {
        tempObjects.clear()
        tempObjects.addAll(objects)
        super.notifyDataSetChanged()
    }
    
    override fun getFilter(): Filter {
        if (!::mFilter.isInitialized) {
            mFilter = CustomStringFilter()
        }
        return mFilter
    }
    
}
0

There are 0 answers