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)
}
}
Related
Im using Recycler View with data Binding to display a list of Routes. For Testing there are always (and only) 10 Items (Route1 -10) Every Route has a delete Button, to delete the Row the User picked(From the List and from the RV). When deleting the first or second Item without scrolling, it works just fine and I can delete all Items. But After scrolling, (I think) the Adapter sets the Position to a wrong value and deletes the wrong items. Eventually the program crashes with a IndexOutOfBoundsException.
I tried to use other positions instead of the int position i get from the Adapter:
holder.absoluteAdapterPosition holder.adapterPosition holder.bindingAdapterPosition holder.layoutPosition
Unfortunately it didnt change the outcome.
Then I tried: Kotlin RecyclerView delete item. Is this a bug?
Works better, but after deleting the last item of the List the adapter position is corrupt again.
Finnaly I wrote the onClicklistener for the delete Button inside the View Holder of my Adapter.
Now it works, but I dont understand why. Can someone please explain me this behavior?
package com.example.testnestedxml
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.testnestedxml.databinding.ListItemBinding
class RouteAdapter(
private var routeList: ArrayList<Route>
) :
RecyclerView.Adapter<RouteViewHolder>() {
//Fügt alle Items (Views) in eine View
private lateinit var binding: ListItemBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RouteViewHolder {
//View wird erstellt
binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return RouteViewHolder(binding)
}
override fun onBindViewHolder(holder: RouteViewHolder, position: Int) {
val route = routeList[position]
holder.routeViewHolderBinding(route, routeList,this)
}
override fun getItemCount(): Int = routeList.size
}
package com.example.testnestedxml
import android.content.Intent
import android.os.Handler
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.example.testnestedxml.databinding.ListItemBinding
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
class RouteViewHolder(
private val binding: ListItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun routeViewHolderBinding(
route: Route,
routeList: ArrayList<Route>,
routeAdapter: RouteAdapter
) {
binding.name.text = route.name
binding.dauer.text = route.duration.toString()
binding.entfernung.text = route.length.toString()
val itemPosition = layoutPosition
binding.delete.setOnClickListener {
routeList.removeAt(itemPosition)
routeAdapter.notifyDataSetChanged()
saveDataRouteAdapter(routeList)
}
}
private fun saveDataRouteAdapter(routeList: ArrayList<Route>) {
val sharedPreferences = binding.root.context.getSharedPreferences(
"shared preferences",
AppCompatActivity.MODE_PRIVATE
)
val editor = sharedPreferences.edit()
val gson = Gson()
val json = gson.toJson(routeList)
editor.putString("Route list", json)
editor.apply()
}
}
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)
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
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()...
}