Home > Software design >  RecyclerView onclick listener is not responsive
RecyclerView onclick listener is not responsive

Time:08-02

I'm currently working on a Wordle clone with Kotlin. So far I've finished building the keyboard with RecyclerView, but there are some issues with the onClick event listener, it's not very responsive. I have to click the key multiple times to make it work, sometimes it just doesn't respond to click at all. I wonder what I did wrong here.

Here's my code for MainActivity

class MainActivity : AppCompatActivity() {
    private var layoutManager: RecyclerView.LayoutManager? = null;
    private var adapter: RecyclerView.Adapter<RecyclerAdapter.ViewHolder>? = null;
    private var row1KeyList = listOf<String>("Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P");
    private var row2KeyList = listOf<String>("A", "S", "D", "F", "G", "H", "J", "K", "L");
    private var row3KeyList = listOf<String>("Z", "X", "C", "V", "B", "N", "M");

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        val recyclerView2 = findViewById<RecyclerView>(R.id.recyclerView2)
        val recyclerView3 = findViewById<RecyclerView>(R.id.recyclerView3)

        recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        recyclerView2.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        recyclerView3.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)

        var rowAdapter1 = RecyclerAdapter(row1KeyList)
        recyclerView.adapter = rowAdapter1
        rowAdapter1.setOnItemClickListener(object : RecyclerAdapter.onItemClickListener{
            override fun onItemClick(position: Int){
//                Toast.makeText(this@MainActivity, row1KeyList[position], Toast.LENGTH_SHORT).show()
                Log.d("letter: ", row1KeyList[position])
                Log.d("position", position.toString())
                val tile = findViewById<TextView>(R.id.tile1)
                tile.text = row1KeyList[position]
            }
        })
        recyclerView2.adapter= RecyclerAdapter(row2KeyList)
        recyclerView3.adapter= RecyclerAdapter(row3KeyList)
    }


}

Here's the code of RecyclerAdapter

class RecyclerAdapter(val keyList: List<String>): RecyclerView.Adapter <RecyclerAdapter.ViewHolder>() {
//        private var keyList = listOf<String>("Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P");
        private var mListener: onItemClickListener? = null

        interface onItemClickListener{
            fun onItemClick(position: Int)
        }

        fun setOnItemClickListener(listener: onItemClickListener){
            mListener = listener
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            val v = LayoutInflater.from(parent.context).inflate(R.layout.key_list, parent, false)
            return ViewHolder(v, mListener)
        }

        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            holder.itemKey.text = keyList[position]
        }

        override fun getItemCount(): Int {
            return keyList.size;
        }

        inner class ViewHolder(itemView: View, listener: onItemClickListener?): RecyclerView.ViewHolder(itemView) {
            var itemKey: AppCompatButton

            init{
                itemView.setOnClickListener {
                    listener?.onItemClick(position)
                }
               itemKey = itemView.findViewById(R.id.letterKey)

            }
        }
}

In case, I also attached the code of the layout file of the key list and the main layout

// key_list.xml defined how each key looks like
<?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        >
        <androidx.appcompat.widget.AppCompatButton
            android:id="@ id/letterKey"
            android:layout_width="24dp"
            android:layout_height="36dp"
            android:background="#D3D6DA"
            android:layout_marginRight="6dp"
            android:textColor="@android:color/black"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>

here's the layout for main activity

<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@ id/recyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.5"
        />
    <androidx.recyclerview.widget.RecyclerView
        android:id="@ id/recyclerView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.6"
        />
    <androidx.recyclerview.widget.RecyclerView
        android:id="@ id/recyclerView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.7"
        />

    <TextView
        android:id="@ id/tile1"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:textSize="10pt"
        android:gravity="center"
        android:background="@drawable/tile_border"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintVertical_bias="0.1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

app screenshot

CodePudding user response:

TRY THIS:

adapter:

    class RecyclerAdapter(val keyList: List<String>, val mListener : ClickListener ) : RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val v = LayoutInflater.from(parent.context).inflate(R.layout.key_list, parent, false)
        return ViewHolder(v, mListener)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.itemKey.text = keyList[position]
    holder.itemKey.setOnClickListener{
       mListener.click(position)
    }
}

// return the number of the items in the list
override fun getItemCount(): Int {
    return keyList.size
}

// Holds the views for adding it to image and text
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val itemKey = itemView.findViewById(R.id.letterKey)

}

interface ClickListener {
  public fun click(position : Int)
}
}

Your new MainActivity:

class MainActivity : AppCompatActivity() : ClickListener{
private var layoutManager: RecyclerView.LayoutManager? = null;
private var adapter: RecyclerView.Adapter<RecyclerAdapter.ViewHolder>? = null;
private var row1KeyList = listOf<String>("Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P");
private var row2KeyList = listOf<String>("A", "S", "D", "F", "G", "H", "J", "K", "L");
private var row3KeyList = listOf<String>("Z", "X", "C", "V", "B", "N", "M");

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
    val recyclerView2 = findViewById<RecyclerView>(R.id.recyclerView2)
    val recyclerView3 = findViewById<RecyclerView>(R.id.recyclerView3)

    recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
    recyclerView2.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
    recyclerView3.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)

    recyclerView.adapter = RecyclerAdapter(row1KeyList, this)
    recyclerView2.adapter= RecyclerAdapter(row2KeyList)
    recyclerView3.adapter= RecyclerAdapter(row3KeyList)
}

override fun click(position : Int){
    Log.d("letter: ", row1KeyList[position])
    Log.d("position", position.toString())
    val tile = findViewById<TextView>(R.id.tile1)
    tile.text = row1KeyList[position]
}
}

CodePudding user response:

I have figured out a solution inspired by Jon's answer. Basically, I just create a listener in the main activity and pass it to the adapter

Here's the updated main activity code:

class MainActivity : AppCompatActivity() {
    private val row1KeyList = listOf<String>("Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P");
    private var row2KeyList = listOf<String>("A", "S", "D", "F", "G", "H", "J", "K", "L");
    private var row3KeyList = listOf<String>("Z", "X", "C", "V", "B", "N", "M");

    interface ItemClickListener {
        fun onItemClick(position : Int, list: List<String>)
    }

     val itemClickListener = object : ItemClickListener {
        override fun onItemClick(position: Int, list: List<String>) {
                Log.d("letter: ", list[position])
                Log.d("position", position.toString())
                val tile = findViewById<TextView>(R.id.tile1)
                tile.text = list[position]
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        val recyclerView2 = findViewById<RecyclerView>(R.id.recyclerView2)
        val recyclerView3 = findViewById<RecyclerView>(R.id.recyclerView3)

        recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        recyclerView2.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        recyclerView3.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)

        recyclerView.adapter = RecyclerAdapter(row1KeyList, itemClickListener)
        recyclerView2.adapter= RecyclerAdapter(row2KeyList, itemClickListener)
        recyclerView3.adapter= RecyclerAdapter(row3KeyList, itemClickListener)
    }

}

here's the updated RecyclerAdapter code:

    class RecyclerAdapter(val keyList: List<String>, val mListener : MainActivity.ItemClickListener): RecyclerView.Adapter <RecyclerAdapter.ViewHolder>() {

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

        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            holder.itemKey.text = keyList[position]
            holder.itemKey.setOnClickListener{
                mListener.onItemClick(position, keyList)
            }
        }

        override fun getItemCount(): Int {
            return keyList.size;
        }

        inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
            var itemKey: AppCompatButton

            init{
               itemKey = itemView.findViewById(R.id.letterKey)
            }
        }
}

CodePudding user response:

You haven't posted your click listener code for the other two rows in the question, so we don't know what's happening there and if there's a problem - but your solution you posted is treating every click listener value as coming from row 1. You need to do something like this:

// Making it a list of lists instead of separate variables will make lookups easier!
// You could keep them as variables, but then you'll have to translate row numbers
// to the correct variable
val rows = listOf(
    listOf("Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"),
    listOf("A", "S", "D", "F", "G", "H", "J", "K", "L"),
    listOf("Z", "X", "C", "V", "B", "N", "M")
)

// we'll look this up once in onCreate and store it here
lateinit var tile: TextView

// this is the function your click listeners will call
fun handleKeyClick(row: Int, position: Int) {
    // look up the key, nice and easy when it's nested lists!
    tile.text = rows[row][position]
}


override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // it's better to find this once and keep a reference to it!
    tile = findViewById<TextView>(R.id.tile1)
    ...
    // pass each RecyclerView the appropriate row of keys, and also a function
    // that takes a position Int. We're using that to call our handler function,
    // and also passing the appropriate row number - it's different in each function
    // for each RecyclerView!
    recyclerView.adapter = RecyclerAdapter(rows[0]) { position -> 
        handleKeyClick(row = 0, position)
    }
    recyclerView.adapter = RecyclerAdapter(rows[1]) { position ->
        handleKeyClick(row = 1, position)
    }
    recyclerView.adapter = RecyclerAdapter(rows[2]) { position ->
        handleKeyClick(row = 2, position)
    }
    ...
}

And then your Adapter needs to handle that function we're passing in:

// the second parameter is different - it's a type that represents a function with
// one Int parameter (the position) and it returns nothing. That's what we're
// passing in when we're creating the adapters in onCreate up there
class RecyclerAdapter(
    val keyList: List<String>,
    val positionClickListener : (Int) -> Unit
) : RecyclerView.Adapter <RecyclerAdapter.ViewHolder>() {

    ...

    // no need to pass a listener in, it's an -inner- class so it can see
    // positionClickListener in the parent class
    inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
        
        init {
            itemView.setOnClickListener {
                // call the listener with the current position this is displaying
                positionClickListener(bindingAdapterPosition)
            }
        }
}

You could set up a click listener in onBindViewHolder instead, but it's more efficient to set it up once per ViewHolder so you're not constantly creating listener objects (in general, doesn't matter here with your fixed layout!)


That should make what you're doing work - your other option is to just pass the key character itself, i.e. make the listener function a (String) -> Unit (you should be working with Chars really) and then the listener can just use the character instead of having to look it up by position.

The other thing I'd say is this isn't really something you should use a RecyclerView for - it's meant for scrolling lists of items, and it reuses ViewHolders to make that efficient instead of creating one for every single item. You have everything displayed on the screen at once, you're not getting any benefit for the extra work it involves!

There are lots of ways to do what you're doing - the simplest would just be a ConstraintLayout with all the key TextViews (they should be Buttons really, especially for accessibility reasons) added to it in rows, using a packed chain to squish them all together in the centre. They could all use the same OnClickListener, which passes the View that was clicked as a parameter, so you can grab the text contents (or a tag) and work out which key was pressed. Just an idea! You've already done the work here so no need to reinvent it

  • Related