Strange behaviour with getFilter() inside RecyclerView adapter

Viewed 104

I am building a small dictioanry app as a side project, screenshot below:

My getFilter() method is working properly and is filtering the results correctly, and when you click on the actual result, a dialog window with information appears. Everything looks fine.

The problem comes when I click on the empty area below the results. Then I start getting dialog windows for items from the full arraylist items, even though they seem to be invisible. It seems like their onClick method gets triggered inside the empty space.

Check screenshot below:

Why is this happening and how can I fix it? Is this a normal filtering behaviour?

Below is the code for my Adapter:

    class RecyclerViewAdapter(private var items: ArrayList<Item>, private val listener: OnItemClickListener) : RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>(), Filterable {
    private var itemsFull = ArrayList(Item)


    inner class ViewHolder (itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
        var item: TextView = itemView.findViewById(R.id.tv_item)
        val icon: ImageButton = itemView.findViewById(R.id.addToFavourites)

        init {
            itemView.setOnClickListener(this)
        }

        override fun onClick(v: View?) {
            val position: Int = absoluteAdapterPosition
            val currentItem = items[position]
            val item = currentItem.itemName
            val definition = currentItem.itemDefinition
            if (position != RecyclerView.NO_POSITION) {
                listener.onItemClick(item,definition,currentItem)
            }
        }
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.item,viewGroup,false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        val currentItem = items[position]
        viewHolder.item.text = currentItem.itemName

        viewHolder.icon.setOnClickListener {
            if (!Constants.favourites.contains(currentItem)) {
                Constants.favourites.add(currentItem)
                viewHolder.icon.startAnimation(AnimationUtils.loadAnimation(viewHolder.icon.context, R.anim.shake))
                Toasty.custom(viewHolder.icon.context, R.string.addedToFavs,R.drawable.ic_star_full,R.color.blue_700,Toast.LENGTH_SHORT,true, true).show()
            }else{
                Toasty.custom(viewHolder.icon.context, R.string.alreadyAdded, R.drawable.ic_attention,R.color.blue_700,Toast.LENGTH_SHORT,true, true).show()
            }
        }
    }

    override fun getItemCount() = items.size

    interface OnItemClickListener {
        fun onItemClick(item: String, definition: String, currentItem: Item)
    }

    override fun getFilter(): Filter {
        return object : Filter() {
            override fun performFiltering(constraint: CharSequence?): FilterResults {
                val results = FilterResults()

                if (constraint==null || constraint.isEmpty()) {
                    results.values = itemsFull
                    results.count = itemsFull.size
                } else {
                    val filteredItems = ArrayList<Item>()
                    for (row in itemsFull) {
                        if (row.itemName.lowercase()
                                .startsWith(constraint.toString().lowercase().trim())
                        ) {
                            filteredItems.add(row)
                        }
                    }
                    results.values = filteredItems 
                    results.count = filteredItems.size
                }

                return results
            }

            @SuppressLint("NotifyDataSetChanged")
            override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
                @Suppress("UNCHECKED_CAST")
                items.clear()
                items.addAll(results?.values as ArrayList<Item>)
                notifyDataSetChanged()
            }
        }
    }
}

The layout containing the RecyclerView:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragments.SearchFragment">

    <LinearLayout
        android:id="@+id/searchLinear"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentEnd="true"
        android:background="@color/blue_700"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:background="@drawable/bg_linear">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:layout_marginLeft="@dimen/_17sdp"
                android:layout_marginStart="@dimen/_17sdp"
                android:layout_marginEnd="@dimen/_17sdp"
                android:layout_marginRight="@dimen/_17sdp"
                android:layout_marginTop="@dimen/_5sdp"
                android:layout_marginBottom="@dimen/_5sdp"
                tools:ignore="UselessParent">

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/alphabet"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:scrollbars="none"
                    tools:listitem="@layout/item_letter"
                    android:overScrollMode="never"/>

            </LinearLayout>

        </LinearLayout>

    </LinearLayout>

    <androidx.cardview.widget.CardView
        android:id="@+id/searchCardView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_alignParentStart="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:layout_below="@id/searchLinear">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/searchRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:scrollbars="none"
            tools:listitem="@layout/item"/>

    </androidx.cardview.widget.CardView>

</RelativeLayout>

UPDATE:

This is how I use the adapter in my Fragment:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val searchView = activity?.findViewById<SearchView>(R.id.searchView)
        searchView?.imeOptions = EditorInfo.IME_ACTION_DONE
        searchView?.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
            override fun onQueryTextSubmit(query: String?): Boolean {
                return false
            }

            override fun onQueryTextChange(newText: String?): Boolean {
                (mAdapter2 as RecyclerViewAdapter).filter.filter(newText)
                return false
            }
        })        

        mRecyclerView2 = binding.searchRecyclerView
        mRecyclerView2.setHasFixedSize(true)
        mRecyclerView2.addItemDecoration(DividerItemDecoration(context,DividerItemDecoration.VERTICAL))
        mLayoutManager2 = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
        mAdapter2 = RecyclerViewAdapter(Constants.wordList as ArrayList<Item>, this)

        mRecyclerView2.layoutManager = mLayoutManager2
        mRecyclerView2.adapter = mAdapter2
    }

My onItemClick method:

override fun onItemClick(item: String, definition: String, currentItem: Item) {
                    val args = Bundle()
                    args.putString("Item", item)
                    args.putString("Definition", definition)

                    val inflatedFragment = InflatedItemFragment()
                    inflatedFragment.arguments = args
                    val fm = requireActivity().supportFragmentManager

                    inflatedFragment.show(fm, "inflatedItem")
                }

UPDATE 2:

I edited the publishResults like so:

override fun publishResults(constraint: CharSequence?, results: FilterResults?) { 
        items.clear() 
        items.addAll(results?.values as ArrayList<Item>) 
        notifyDataSetChanged() 
    } 

Now when I click on the empty area I get an error:

2021-08-28 13:23:19.220 8125-8125/com.jpdevzone.knowyourwords
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.jpdevzone.knowyourwords, PID: 8125
    java.lang.IndexOutOfBoundsException: Index: 3, Size: 1
        at java.util.ArrayList.get(ArrayList.java:437)
        at com.jpdevzone.knowyourwords.adapters.RecyclerViewAdapter$ViewHolder.onClick(RecyclerViewAdapter.kt:29)

and its pointing to this line inside the onClick method:

val currentItem = items[position]
1 Answers

SOLUTION:

Adding notifyItemRangeChanged(0,items.size) to publishResults inside getFilter() solved the problem.

@SuppressLint("NotifyDataSetChanged")
@Suppress("UNCHECKED_CAST")
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
    items.clear()
    items.addAll(results?.values as ArrayList<Item>)
    notifyItemRangeChanged(0,items.size)
    notifyDataSetChanged()
}
Related