2

我创建了自己的自定义过滤器视图,就像自动完成文本视图一样。

因此,为了解释,我将完整列表传递给适配器,在用户输入 3 个或更多字符后,我然后过滤列表并使用 diff util 显示。这一切都在我的适配器中完成(见下文)

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()
    }

还有我的 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
}

所以这很好用,过滤显示很好,如果我删除或添加回一个项目。如果列表很大(350 多个项目),则会出现问题。我读过最好在后台线程上进行处理的文档,所以我这样做了。我将 diff util 调用改回:

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]
}

更新了我的适配器以扩展RecyclerView.Adapter并在后台线程上运行,例如

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

不幸的是,这没有用,所以我尝试使用协程(我几乎没有或没有经验,所以我不能确定它们是否有效)

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

像这样调用上面的代码: CoroutineScope(Dispatchers.Default).launch {update(callback)}

然而,这也不起作用,UI 阻塞仍在发生。所以我将适配器恢复为扩展 aListAdapter并使用AsyncListDiffer

初始化如下val differ = AsyncListDiffer(this, SuggestedLocationDiffCallback())。并像使用它一样differ.submitList(newList)

不幸的是,这也不起作用,UI 阻塞仍在继续。我开始寻找分页的道路,我希望这可以解决问题,但是任何人都可以看看上面的内容,看看我是否做错了什么?

这里也是封装视图类:

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

当我把头发拉出来时,任何帮助都会受到赞赏。

编辑 因此,经过更多调查,似乎不是导致问题的 diffcallback 。我在绑定方法中添加了一个日志,它被称为x次数,从最后一个到显示在屏幕上有很长的延迟,bind并且在此期间ui被阻塞。

4

0 回答 0