I have a RecyclerView filled with different images, when the user click one of then i apply a border to highlight the image. Everything is working fine, however, the user can click multiple images and all of then get highlighted, i want to select only one at a time. I have search over multiples sites and posts however none of them have a solution that works for me. Here is an image:
I am using a ImageView click listener, not an ItemClickListener.
Here is the adapter code:
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
class AccountViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var accountImage: ImageView =
view.findViewById(R.id.account_image_placeholder)
}
class AddEditAccountAdapter(private var context: Context, private var
accountImages: ArrayList<String>) :
RecyclerView.Adapter<AccountViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
AccountViewHolder {
val imageItem = LayoutInflater.from(context).inflate(
R.layout.account_image_item,
parent, false
)
return AccountViewHolder(imageItem)
}
override fun getItemCount(): Int {
return accountImages.size
}
override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
val accountImageId =
context.getResources().getIdentifier(accountImages.get(position), "drawable", context.getPackageName())
holder.accountImage.setImageResource(accountImageId)
holder.accountImage.setOnClickListener {
holder.accountImage.setBackgroundResource(R.drawable.image_highlight)
}
}
}
you should have a global field to hold your selected position like below:
var selectedPos = -1 // hold selected position in your adapter
// in your bindView, because your view will be reused, you should always check the selected position to set the suitable background
holder.accountImage.setBackgroundResource(if(selectedPos == position) yourImageHightlight else yourNormalImage)
holder.accountImage.setOnClickListener {
selectedPos = position
notifyDataSetChanged()// or something like notifyItemChanged()...
}
Related
I am relativley new to Android Studio and I have tried making a RecyclerView for my Application. I have ran into an issue where I cannot update the ViewHolder unless it's inside the ViewHolder itself.
Here is my RecyclerViewAdapter
package com.example.testactivity
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
class RecyclerViewAdapter() : RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var name : TextView
var description : TextView
var listCard : CardView
init {
name = itemView.findViewById(R.id.projname)
description = itemView.findViewById(R.id.desc)
listCard = itemView.findViewById(R.id.listCard)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.recyclerview, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: RecyclerViewAdapter.ViewHolder, position: Int) {
holder.name.text = ideaList[position].name
holder.description.text = ideaList[position].description
holder.listCard.setCardBackgroundColor(Color.parseColor(ideaList[position].color))
holder.itemView.findViewById<LinearLayout>(R.id.linLayout).setOnClickListener(View.OnClickListener {
ideaList.remove(ideaList[position])
println(position.toString())
notifyItemChanged(ideaList.size)
})
}
override fun getItemCount(): Int {
return ideaList.size
}
fun addItem(name: String, desc: String, color: String) {
val idea = Idea(
name,
desc,
color
)
ideaList.add(idea)
notifyItemInserted(ideaList.size-1)
println((ideaList.size-1).toString())
}
}
And this is my MainActivity
class MainActivity : AppCompatActivity() {
//private var layoutManager: LayoutManager? = null
//private var adapter: RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>? = null
override fun onCreate(savedInstanceState: Bundle?) {
lateinit var layoutManager : LayoutManager
lateinit var adapter : RecyclerViewAdapter
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Switch Activity
val accbtn = findViewById<ImageButton>(R.id.accInf)
accbtn.setOnClickListener {
val i = Intent(this, SecondActivity::class.java)
//i.putExtra("", value) **IMPORT THE ACCOUNT INFORMATION ONTO THIS PAGE
this.startActivity(i)
this.overridePendingTransition(0, 0);
}
// Add Item
val addbtn = findViewById<ImageButton>(R.id.addBtn)
val bottomSheet = BottomSheetFragment()
// Use the add button to open a modal to add an item to a grid
addbtn.setOnClickListener {
//Open bottom sheet modal to add item
bottomSheet.show(supportFragmentManager, "BottomSheetDialog")
}
// Add Item to Recycler view
val recycleView = findViewById<RecyclerView>(R.id.recyclerview)
layoutManager = LinearLayoutManager(this)
adapter = RecyclerViewAdapter()
recycleView.layoutManager = layoutManager
recycleView.adapter = adapter
}
I have tried using it in the MainActivity and inside of the ViewAdapter, aswell when I did "notifyItemChanged(ideaList.size)" inside of the ViewHolder it worked, but it doesn't work with any of the notify functions outside of this.
Not only does notifyOnItemChanged work anywhere, but the onBindViewHolder is the one place you should probably never call it- the point of that function is to bind a view, preferably without side effects. Luckily you aren't doing that, you're doing it in a callback set in that function which is totally different.
THe reason your call doesn't work is the call is wrong. Your code is:
ideaList.remove(ideaList[position])
println(position.toString())
notifyItemChanged(ideaList.size)
Let's say the size of the list is 10, and the position is 4 at the beginning of this function. First off, you didn't change anything- you removed something. Changed would be if you swapped the value at position X with a different value, but kept the size and placement unchanged. So it should be notifyItemRemoved, not notifyItemChanged. Secondly, the index you send is wrong- you'd be sending 9 in my example above. You should be sending 4, the index of the item removed. So you want notifyItemRemoved(position)
So I have found the source of the problem. Inside my Adapter for my Recyclerview I am trying to check if the imageName is null or empty, if it isn't then we can get the image from the local storage and put it into the holder's ImageView. When doing this check and only having an image in the first item in the list of items in the adapter, the image will be placed in multiple positions... And if I keep spinning the recyclerview it continuously adds more. The reason I want to do this is because I want a default image of a plus icon to trigger more logic, otherwise the image that's stored if it's available.
Below is the first part of the code that creates the issue, and some images for you to see what's happening.
package com.example.dukebox.adapters
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.dukebox.FORWARD_SLASH
import com.example.dukebox.R
import com.example.dukebox.RECORD_IMAGES
import com.example.dukebox.models.Record
import com.example.dukebox.viewmodel.HomeViewModel
class NumberSelectionAdapter(
private val recordInfoTitle: TextView,
private val viewModel: HomeViewModel
): RecyclerView.Adapter<NumberSelectionAdapter.ViewHolder>() {
var selectedPosition = 0
private var records: List<Record> = emptyList()
private val imagesPath = "${recordInfoTitle.context.getExternalFilesDir(null)?.absolutePath}$RECORD_IMAGES$FORWARD_SLASH"
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.number_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val record = records[position]
if (!record.imageName.isNullOrEmpty()){
Glide.with(recordInfoTitle.context).load("$imagesPath$FORWARD_SLASH${record.imageName}").into(holder.recordCover)
}
}
override fun getItemCount(): Int {
return records.size
}
fun updateList(newRecords: List<Record>) {
records = newRecords
notifyDataSetChanged()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val recordCover: ImageView = itemView.findViewById(R.id.recordCover)
}
}
And then the code that only puts the image where it's supposed to be the entire time. As you can see, the only difference is the null or empty check in the onBindViewHolder.
package com.example.dukebox.adapters
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.dukebox.FORWARD_SLASH
import com.example.dukebox.R
import com.example.dukebox.RECORD_IMAGES
import com.example.dukebox.models.Record
import com.example.dukebox.viewmodel.HomeViewModel
class NumberSelectionAdapter(
private val recordInfoTitle: TextView,
private val viewModel: HomeViewModel
): RecyclerView.Adapter<NumberSelectionAdapter.ViewHolder>() {
var selectedPosition = 0
private var records: List<Record> = emptyList()
private val imagesPath = "${recordInfoTitle.context.getExternalFilesDir(null)?.absolutePath}$RECORD_IMAGES$FORWARD_SLASH"
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.number_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val record = records[position]
Glide.with(recordInfoTitle.context).load("$imagesPath$FORWARD_SLASH${record.imageName}").into(holder.recordCover)
}
override fun getItemCount(): Int {
return records.size
}
fun updateList(newRecords: List<Record>) {
records = newRecords
notifyDataSetChanged()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val recordCover: ImageView = itemView.findViewById(R.id.recordCover)
}
}
And some pictures of what it looks like, because it tries to load an image that doesn't exist, the plus signs aren't there. But the image is only where it's suppose to be. I span it multiple times, and there is only ever one.
RecyclerViews reuse (recycle) their ViewHolders, so in onBindViewHolder you're usually getting one that's already displaying stuff for another item. You need to update it so it looks right for your current item.
Here's what you're doing
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val record = records[position]
// this ONLY updates IF there's an imageName
if (!record.imageName.isNullOrEmpty()){
Glide.with(recordInfoTitle.context).load("$imagesPath$FORWARD_SLASH${record.imageName}").into(holder.recordCover)
}
}
The problem is you set a new pic if there's an image name - but you don't clear it if there isn't. So if you're given a ViewHolder that was previously displaying an item with a image, that image is still there!
If you don't explicitly remove it, it's still going to show that old data. That goes for anything in onBindViewHolder - you need to update everything to the correct display state for the current item. That's why you're seeing the image "repeat" when it should only be on the first item - it's just the ViewHolder that was used for the first item showing up again, and unless you change the contents of recordCover, it's gonna be there every time
Since you're using Glide, you should use its clear() function to wipe the image, as it explains in the docs:
By calling clear() or into(View) on the View, you’re cancelling the load and guaranteeing that Glide will not change the contents of the view after the call completes. If you forget to call clear() and don’t start a new load, the load you started into the same View for a previous position may complete after you set your special Drawable and change the contents of the View to an old image.
Basically, because you're telling Glide to load an image into a particular ViewHolder's ImageView, and that can happen asynchronously, it's possible (if you're scrolling fast) that you'll get to another item that uses that ViewHolder, set the contents to blank, but then Glide will show up after a delay with that earlier requested image and set it on that ImageView.
By using the clear() call through Glide, it can internally keep track of what jobs are queued up, and it can cancel a request for that ImageView if a newer one comes in, including a request to clear it. Let Glide take care of it all, basically!
So:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val record = records[position]
// always update the ImageView to the correct state
if (!record.imageName.isNullOrEmpty()){
Glide.with(recordInfoTitle.context).load("$imagesPath$FORWARD_SLASH${record.imageName}").into(holder.recordCover)
} else {
Glide.with(recordInfoTitle.context).clear(holder.recordCover)
}
}
MyAdapter.kt
im a beginner in this and how to open another activity when i click the cardview..previously it worked fine,but when i click the card nothing happens and only toast show up
I want the user to be able to click on a card and go to a different activity. If you click on card 1 you must go to the activity1. If you click on card 2 you must go to the activity2. Etc...
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.viewpager.widget.PagerAdapter
import kotlinx.android.synthetic.main.card_item.view.*
class MyAdapter(private val context: Context, private val myModelArrayList: ArrayList<MyModel>) : PagerAdapter(){
override fun getCount(): Int {
return myModelArrayList.size //return list of records/items
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
//inflate layout card_item.xml
val view = LayoutInflater.from(context).inflate(R.layout.card_item, container, false)
//get data
val model = myModelArrayList.get(position)
val title = model.title
val description = model.description
val date = model.date
val image = model.image
//set data to ui views
view.bannerIv.setImageResource(image)
view.titleTv.text = title
view.descriptionTv.text = description
view.dateTv.text = date
view.setOnClickListener {
val intent = Intent(context, SecondActivity::class.java)
context.startActivity(intent)
// finish();
}
//handle item/card click
view.setOnClickListener{
Toast.makeText(context,"$title \n $description \n $date", Toast.LENGTH_SHORT).show()
}
//add view to container
container.addView(view, position)
return view
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
}
```
just simply remove your second view.setOnClickListener and move your toast to first view.setOnClickListener. As others mentioned second listener will be overridden.
I'm developing an android app in kotlin, and I want to have a button in every recyclerView element, which will launch an intent - the same in whole recycle view, but with different parameters(for now it's just position for testing, in final form that will be some value from database).
I write the following code for that(inside my adapter class):
override fun onBindViewHolder(holder: ProjectViewHolder, position: Int) {
val Edit: Button = holder.view.EditButton
Edit.setOnClickListener()
{
var projekt: Intent = Intent(applicationContext, Project::class.java)
projekt.putExtra("id", position)
startActivity(projekt)
}
But I get "unresolved refference" error for applicationContext. I used buttons with intent like that before and that worked perfectly fine, though this is the first time I'm trying to do it inside recyclerView element.
How to make it work? Maybe I just take the wrong approach and it should be done in different way?
Edit: Complete adapter class file:
package com.example.legoapp127260
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.project_item_layout.view.*
class ProjectAdapter : RecyclerView.Adapter<ProjectViewHolder>()
{
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ProjectViewHolder {
val layoutInflater = LayoutInflater.from(viewGroup.context)
val projectRow = layoutInflater.inflate(R.layout.project_item_layout, viewGroup, false)
return ProjectViewHolder(projectRow)
}
override fun getItemCount(): Int {
return 2;
}
override fun onBindViewHolder(holder: ProjectViewHolder, position: Int) {
val projectName: TextView = holder.view.projectName
val projectNames: Array<String> = arrayOf("Set 1", "Set 2")
val Edit: Button = holder.view.EditButton
projectName.setText(projectNames[position])
Edit.setOnClickListener()
{
var projekt: Intent = Intent(Edit.context, Project::class.java)
projekt.putExtra("id", position)
Edit.context.startActivity(projekt)
}
}
}
class ProjectViewHolder(val view: View) : RecyclerView.ViewHolder(view)
{
}
You can get context from your button:
var projekt: Intent = Intent(Edit.context, Project::class.java)
projekt.putExtra("id", position)
Edit.context.startActivity(projekt)
I'am trying to handle my recyclerview put there is a problem
when i scroll down (onBindView function) works fine
but when scroll back to the first items in recyclerview everything becomes wrong like the following images.
package com.leaderspro.mrlawyer.adapters
import android.graphics.Paint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.leaderspro.mrlawyer.R
import com.leaderspro.mrlawyer.models.TODOModel
import kotlinx.android.synthetic.main.todo_list.view.*
class TODOAdapter(private val mArray: ArrayList<TODOModel>) :
RecyclerView.Adapter<TODOAdapter.ViewHolder>() {
var mView: View? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
mView = LayoutInflater.from(parent.context).inflate(R.layout.todo_list, parent, false)
return ViewHolder(mView!!)
}
override fun getItemCount(): Int {
return mArray.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val mTODO = mArray[position]
if (mTODO.isDone == 0) {//not complete
holder.ivIsDone.setImageResource(R.drawable.ic_checkbox_unchecked)
} else if (mTODO.isDone == 1) {
holder.ivIsDone.setImageResource(R.drawable.ic_checkbox_checked)
holder.tvTodoTask.paintFlags =
holder.tvTodoTask.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG //put line on done Tasks
}
holder.tvTodoTask.text = mTODO.task
holder.tvTODODate.text = mTODO.date
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvTodoTask = itemView.tvTodoTask!!
val tvTODODate = itemView.tvTODODate!!
val ivIsDone = itemView.ivIsDone!!
val todoListMainLinear = itemView.todoListMainLinear!!
}
}
works fine
https://i.stack.imgur.com/6mBUJ.png
works fine
https://i.stack.imgur.com/qcpLK.png
Wrongly called onBindView when Scroll Back
https://i.stack.imgur.com/2sRsu.png
use holder.adapterposition instead of position
val mTODO = mArray[holder.adapterposition]
for more info check: https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.Adapter.html?hl=en#onBindViewHolder(VH,%20int)
edit:
as #Pawel has pointed out, the flags should be cleared when mTodo.isDone == 0.
that should get the job done