2

I have created my own custom filter view, much like the autocomplete text view.

So to explain, I pass in the full list to the adapter, after the user enters 3 or more characters I then filter the list and display using diff util. This is all done in my adapter (see below)

import android.os.Handler
import android.support.v7.recyclerview.extensions.ListAdapter
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Filter
import android.widget.Filterable
import kotlinx.android.synthetic.main.autocomplete_item_layout.view.*

private const val EXACT_MATCH = 1
private const val STARTS_WITH_AND_CONTAINED_ONCE = 2
private const val STARTS_WITH_AND_CONTAINED_MORE_THAN_ONCE = 3
private const val SEARCH_TERM_IS_CONTAINED_MORE_THAN_ONCE = 4
private const val SEARCH_TERM_IS_CONTAINED_ONCE = 5

class AutoCompleteAdapter (private val locations: MutableList<String>, private val filteredlistener: FilteredResultsListener, private val suggestedLocationClickListener : (AutoCompleteLocationSuggestion) -> Unit) :
        ListAdapter<String, AutoCompleteAdapter.AutoCompleteViewHolder>(SuggestedLocationDiffCallback()), Filterable {

    var initalFilteredLocation = ArrayList<String>()
    var filteredLocationSuggestions = ArrayList<String>()
    private var currentSearchTerm = ""

    override fun getFilter(): Filter = autoSuggestionFilter

    private val locationComparatorForSorting = Comparator {
        s1: String, s2: String ->  calculateRank(s1, currentSearchTerm) - calculateRank(s2, currentSearchTerm)
    }

    private fun calculateRank(value: String, searchTerm: String): Int {
        val cleanedValue = value.trim().toLowerCase()
        val startsWithSearchTerm = cleanedValue.startsWith(searchTerm)
        val searchTermCount = cleanedValue.countSubString(searchTerm)
        // rule 1
        if (searchTerm == cleanedValue) {
            return EXACT_MATCH
        }
        // rule 2
        if (startsWithSearchTerm && searchTermCount == 1) {
            return STARTS_WITH_AND_CONTAINED_ONCE
        }
        // rule 3
        if (startsWithSearchTerm && searchTermCount > 1) {
            return STARTS_WITH_AND_CONTAINED_MORE_THAN_ONCE
        }

        // rule 4
        if (searchTermCount > 1) {
            return SEARCH_TERM_IS_CONTAINED_MORE_THAN_ONCE
        }
        // rule 5
        return SEARCH_TERM_IS_CONTAINED_ONCE
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AutoCompleteViewHolder
            = AutoCompleteViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.autocomplete_item_layout, parent, false))

    override fun onBindViewHolder(holder: AutoCompleteViewHolder, position: Int) {
        val location = getItem(position)
        holder.bind(location, suggestedLocationClickListener)
    }

    private fun handleRecyclerViewVisibility() {
        filteredlistener.hasFilteredResults(filteredLocationSuggestions.isNotEmpty())
    }

    fun addSuggestedLocation(suggestion: AutoCompleteLocationSuggestion) {
        val newList = ArrayList<String>()
        newList.addAll(filteredLocationSuggestions)
        newList.add(suggestion.position, suggestion.location)
        submitList(newList)
        filteredLocationSuggestions = newList
    }

    fun removeSuggestedLocation(suggestion: AutoCompleteLocationSuggestion) {
        val newList = ArrayList<String>()
        newList.addAll(filteredLocationSuggestions)
        newList.removeAt(suggestion.position)
        submitList(newList)
        filteredLocationSuggestions = newList
    }

    fun clearSuggestedLocations() {
        filteredLocationSuggestions.clear()
        val newList = ArrayList<String>()
        submitList(newList)
    }

    fun addAll(selectedItems : ArrayList<AutoCompleteLocationSuggestion>) {
        val newList = ArrayList<String>()
        newList.addAll(filteredLocationSuggestions)
        selectedItems.forEach {
            newList.add(it.position, it.location)
        }
        submitList(newList)
    }

    class AutoCompleteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(locationName: String, clickListener: (AutoCompleteLocationSuggestion) -> Unit) {
            itemView.auto_complete_suggestion_text.text = locationName
            if (itemView.location_check_box.isChecked) {
                itemView.location_check_box.isChecked = false
            }
            itemView.setOnClickListener {
                itemView.location_check_box.isChecked = true
                Handler().postDelayed({
                    clickListener(AutoCompleteLocationSuggestion(adapterPosition, locationName))
                }, 200)
            }
        }
    }

    private var autoSuggestionFilter: Filter = object : Filter() {

        override fun performFiltering(constraint: CharSequence?): FilterResults {
            val searchConstraint = cleanSearchTerm(constraint)
            currentSearchTerm = searchConstraint
            val filteredList = locations.filter { it.toLowerCase().contains(searchConstraint) }

            val filterResults = FilterResults()
            filterResults.values = filteredList
            filterResults.count = filteredList.size

            return filterResults
        }

        override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
            if (results.count == 0) {
                return
            }

            filteredLocationSuggestions = results.values as ArrayList<String>
            initalFilteredLocation = filteredLocationSuggestions

            filteredLocationSuggestions.sortWith(locationComparatorForSorting)
            submitList(filteredLocationSuggestions)

            handleRecyclerViewVisibility()
        }
    }

        private fun cleanSearchTerm(constraint: CharSequence?): String = constraint.toString().trim().toLowerCase()
    }

And my SuggestedLocationDiffCallback() :

import android.support.v7.util.DiffUtil

class SuggestedLocationDiffCallback : DiffUtil.ItemCallback<String>() {
    override fun areItemsTheSame(oldItem: String?, newItem: String?): Boolean = oldItem == newItem

    override fun areContentsTheSame(oldItem: String?, newItem: String?): Boolean = oldItem == newItem
}

So this worked fine, the filtering displayed fine and if I removed or added back in an item. The problem occurs if the list is any way large (350+ items). I had read in the docs that best do the processing on a background thread so I did that. I changed my diff util call back to:

import android.support.v7.util.DiffUtil

class SuggestedDiffCallback(private val newList: MutableList<String>, private val oldList: MutableList<String>): DiffUtil.Callback() {

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean  = oldList[oldItemPosition] == newList[newItemPosition]

    override fun getOldListSize(): Int = oldList.size

    override fun getNewListSize(): Int = newList.size

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = oldList[oldItemPosition] == newList[newItemPosition]
}

Updated my adapter to extend RecyclerView.Adapter and ran on a background thread e.g.

            if (handler == null) handler = Handler()
            Thread(Runnable {
                val diffResult = DiffUtil.calculateDiff(callback)
                handler!!.post(Runnable {
                    diffResult.dispatchUpdatesTo(this@AutoCompleteAdapter)
                })
            }).start()

This unfortunately didn't work, so I then attempted to use coroutines (I have little or no experience so I can't say for sure they worked)

private suspend fun update(callback: SuggestedDiffCallback) {
        withContext(Dispatchers.Default) {
            val diffResult = DiffUtil.calculateDiff(callback)
            diffResult.dispatchUpdatesTo(this@AutoCompleteAdapter1)
        }
        handleRecyclerViewVisibility()
    }

Calling the above like so: CoroutineScope(Dispatchers.Default).launch {update(callback)}

This however didn't work either, the UI blocking was still occurring. So I reverted the adapter back to extending a ListAdapter and use AsyncListDiffer

initialising as follows val differ = AsyncListDiffer(this, SuggestedLocationDiffCallback()) . and using it like differ.submitList(newList)

Unfortunately this did not work either and the UI blocking continues. I started to look down the road of pagination, which I hope may solve the issue but can anyone look at the above and see if I'm doing anything wrong?

Also here is the encapsulating view class:

import android.arch.paging.PagedList
import android.arch.paging.PagedListAdapter
import android.content.Context
import android.support.constraint.ConstraintLayout
import android.support.v4.widget.NestedScrollView
import android.util.AttributeSet
import android.view.inputmethod.InputMethodManager
import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.auto_complete_component_layout.view.*
import java.util.*

private const val MAX_TAGS = 10
private const val FILTER_CHAR_COUNT = 3
class AutocompleteLocationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
                                                         defStyleAttr: Int = 0): ConstraintLayout (context, attrs, defStyleAttr), SuggestedLocationUpdateListener, FilteredResultsListener {

    private var maxLocationsAddedListener: MaxLocationsAddedListener? = null
    private var adapter: AutoCompleteAdapter1? = null
    private var pagedListAdapter: LocationPagedListAdapter? = null
    private val selectedItems : ArrayList<AutoCompleteLocationSuggestion> = ArrayList()
    private var applyButtonVisibilityListener: ApplyButtonVisibilityListener? = null
    private var suggestionListVisibleCallback: SuggestionListVisibleCallback? = null
    private val disposables = CompositeDisposable()

    init {
        inflate(context, R.layout.auto_complete_component_layout, this)
        observeClearSearchTerm()
    }

    fun setLocations(locations: MutableList<String>) {
        pagedListAdapter = LocationPagedListAdapter(locations, SuggestedLocationDiffCallback(), this, this::suggestedLocationClicked)
        //adapter = AutoCompleteAdapter(locations, this, this::suggestedLocationClicked)
        location_suggestion_list.adapter = pagedListAdapter
        location_search_box.onChange { handleSearchTerm(it)}
        scrollview.setOnScrollChangeListener(onScrollChangeListener)
    }

    private fun handleSearchTerm(searchTerm: String) {
        if(searchTerm.length >= FILTER_CHAR_COUNT) {
            pagedListAdapter?.filter?.filter(searchTerm)
        } else {
            location_suggestion_list.visible(false)
            suggestionListVisibleCallback?.areSuggestionsVisible(false)
            adapter?.clearSuggestedLocations()
        }
    }

    private fun observeClearSearchTerm() {
        disposables + clear_search_term
                .throttleClicks()
                .subscribe {
                    handleClearLocationSearchBox()
                }
    }

    private fun handleClearLocationSearchBox() {
        location_search_box.text.clear()
        if (!selectedItems.isNullOrEmpty()) {
            location_search_box.hint = context.getString(R.string.add_another_location)
        }
    }

    fun setSuggestionListVisibleCallback(suggestionListVisibleCallback: SuggestionListVisibleCallback) {
        this.suggestionListVisibleCallback = suggestionListVisibleCallback
    }

    fun clearAreas() {
        adapter?.addAll(selectedItems)
        selectedItems.clear()
        selected_location_container.removeAllViews()
    }

    fun setMaxLocationsAddedListener(maxLocationsAddedListener: MaxLocationsAddedListener) {
        this.maxLocationsAddedListener = maxLocationsAddedListener
    }

    fun setApplyButtonListener(applyButtonVisibilityListener: ApplyButtonVisibilityListener) {
        this.applyButtonVisibilityListener = applyButtonVisibilityListener
    }

    private fun suggestedLocationClicked(suggestedLocation: AutoCompleteLocationSuggestion) {
        val selectedItemView = SelectedSuggestionView(context, null, 0, this)
        selectedItemView.setAutoCompleteSuggestion(suggestedLocation)
        if (selected_location_container.childCount == MAX_TAGS) {
            maxLocationsAddedListener?.maxLocationsAdded()
        } else {
            selected_location_container.addView(selectedItemView, 0)
            selectedItems.add(suggestedLocation)
            adapter?.removeSuggestedLocation(suggestedLocation)
            applyButtonVisibilityListener?.isApplyButtonVisible(selectedItems.isNotEmpty())
            hideKeyboard()
        }
    }

    private fun updateFilteredResults() {
        selectedItems.forEach {
            if (adapter?.filteredLocationSuggestions?.contains(it.location)!!) {
                adapter?.filteredLocationSuggestions?.remove(it.location)
            }
        }
    }

    fun submitList(suggestedLocations: PagedList<String>?) {
        pagedListAdapter?.submitList(suggestedLocations)
    }

    fun getSelectedItems() : ArrayList<String> = selectedItems.map { it.location } as ArrayList<String>

    override fun hasFilteredResults(results: Boolean) {
        location_suggestion_list.visible(results)
        suggestionListVisibleCallback?.areSuggestionsVisible(results)
    }

    override fun selectedLocationDeselected(suggestedLocation: AutoCompleteLocationSuggestion) {
        if (adapter?.filteredLocationSuggestions?.isNotEmpty()!!) {
            selectedItems.remove(suggestedLocation)
            adapter?.addSuggestedLocation(suggestedLocation)
            applyButtonVisibilityListener?.isApplyButtonVisible(selectedItems.isNotEmpty())
        }
    }

    fun addSelectedItemsToCloud(suggestedLocations: ArrayList<String>) {
        suggestedLocations.forEach {
            val selectedItemView = SelectedSuggestionView(context, null, 0, this)
            selectedItemView.setAutoCompleteSuggestionText(it)
            selected_location_container.addView(selectedItemView)
        }
    }

    interface MaxLocationsAddedListener {
        fun maxLocationsAdded()
    }

    private var onScrollChangeListener: NestedScrollView.OnScrollChangeListener = NestedScrollView.OnScrollChangeListener { _, _, _, _, _ -> hideKeyboard() }

    private fun hideKeyboard() {
        val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(windowToken, 0)
    }
}

any help is appreciated on this as I'm pulling my hair out.

EDIT So after more investigation, it may seem that it's not the diffcallback that is causing the issue. I added a log in the bind method, it is called x amount of times, and there is a lengthy delay from the last bind to being displayed on screen and the ui is blocked for this duration.

DJ-DOO
  • 4,219
  • 15
  • 48
  • 91

0 Answers0