I have a custom view in which you can draw with your fingers, I fill out a recyclerview with this view, made an adapter, and for example a list with size = 10 is obtained, where each item is a custom view, and if the user draws for example in item with position 5 then when scrolling the same picture appears in another item where he did not draw.
here is my adapter:
class CustomViewListAdapter : RecyclerView.Adapter<CustomViewListAdapter.ViewHolder>() {
private var listSchedule = ArrayList<Int>()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var calendarView: CustomView= itemView.calendar!!
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.schedule_item,
parent,
false
)
)
}
override fun getItemCount(): Int = listSchedule.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.calendarView.setValue(listSchedule[position] )
}
fun setSchedules(listSchedule: ArrayList<Int>) {
this.listSchedule = listSchedule
notifyDataSetChanged()
}
}
Are you aware of how RecyclerView works? Lets assume you have 1000 items in your list. A simple approach would be to create 1000 views from your schedule_item layout bind them and show them in e.g. a scrollview. However, doing that would cost a lot of time and memory. So the clever Android developers came up with the following idea:
Since of the 1000 items only e.g. 10 are visible at the same time, lets just create 10 views and only change the content of the views depending on the actual item they show. So the 10 views are reused or recycled.
For this to work, the onBindViewHolder implementation must make sure that it updates the provided viewholder in a way that it shows the content of the item at the given position.
Now in your code, all you do in onBindViewHolder is, to set a single integer. There is no sign of setting any custom drawing etc. So I assume the drawing is just stored inside the CustomView. And since there are only those 10 or so CustomViews (as explained above), when they are reused to show a different item, they contain the original drawings because you didn't change that in onBindViewHolder.
Related
I just saw this example class for an Adapter for Recyclerview and I'm a bit confused on how it knows to call onCreateViewHolder, onBindViewHolder, etc just from adding an Item object to a list?
Does it have something to do with the line notifyItemInserted(items.size - 1) ?
Is it that whenever this method is called the onCreateViewHolder method is recalled with for that item or?
Adapter:
class ListAdapter (
private val items: MutableList<Item>
) : RecyclerView.Adapter <ListAdapter.ListViewHolder>() {
class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return ListViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.list_items, parent, false)
)
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
val currItem = items[position]
holder.itemView.apply {
tv_item.text = currItem.title
cb_item.isChecked = currItem.checked
crossItem(tv_item, currItem.checked)
cb_item.setOnCheckedChangeListener { _, isChecked ->
crossItem(tv_item, isChecked)
currItem.checked = !currItem.checked
items.removeAll { item ->
item.checked
}
notifyDataSetChanged()
}
}
}
override fun getItemCount(): Int {
return items.size
}
private fun crossItem (itemText: TextView, checked: Boolean) {
if (checked){
//dk wtf paint flags is
itemText.paintFlags = itemText.paintFlags or STRIKE_THRU_TEXT_FLAG
}
//else remove
else {
itemText.paintFlags = itemText.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
fun addItem (item: Item){
items.add (item)
notifyItemInserted(items.size - 1)
}
}
Item Class:
data class Item (
val title: String,
var checked: Boolean = false
)
{
}
Whenever the Adapter needs to provide a new view for the RecyclerView to draw, it checks if it has an unused ViewHolder in its pool. If it doesn't, it calls onCreateViewHolder() so it can create one. Then it calls onBindViewHolder() for the ViewHolder that came from either source so the contained view can be prepared before being added to the layout.
If you call one of the notify methods, that triggers it to refresh whichever item rows are affected. It will return any removed rows to the ViewHolder pool and then follow the above steps to get the views it needs for new rows. If you use a notify...changed method, it will only need to use onBindViewHolder() for the applicable rows. When you use the nuclear option notifyDataSetChanged(), it returns all items to the pool.
When the RecyclerView is first displayed, or when the layout is resized, those actions will possibly trigger the need to show more rows. When you scroll the list, items that scroll off the screen are returned to the ViewHolder pool, and when new items scroll into view, ViewHolders need to be created or acquired from the pool as explained above.
By the way, this is going to look ugly because it refreshes the whole list even though only some items are removed:
items.removeAll { item ->
item.checked
}
notifyDataSetChanged()
I recommend this instead so you get a nice transition:
for (i in items.indices.reversed()) {
if (items[i].checked) {
items.removeAt(i)
notifyItemRemoved(i)
}
}
I iterate in reverse so the indices that are removed are stable as you iterate and remove items.
override fun getItemCount(): Int {
return items.size
}
This function is the key, it knows how many to create and how many to bind by knowing how many there are in total. The amount of ViewHolders created is more based on how many Views can fit on the screen at one time.
This gets more complex when you have different view types, as it will sometimes has to create more ViewHolders than what was required from the start as view types change.
The notify... functions just let the Adapter know it needs to "re-look" at the List.
I would like to made the rows bold that starts with the word "Main".
This is inside an adapter of a RecycleView, hence some limitations are expected. e.g. I can't obtain the view via FindViewById().
My solution below works, but it feels very wrong, given I'm importing server_detail_id.
import kotlinx.android.synthetic.main.row_item_detail.view.server_detail_id
class ServerDetailAdapter(private var serverList: ArrayList<ServerDetailRow>, context: Context?) :
RecyclerView.Adapter<DefaultViewHolder>() {
override fun onBindViewHolder(holder: DefaultViewHolder, position: Int) {
val serverRow: ServerDetailRow = serverList[position]
val server = serverRow.server
val context = holder.itemView.context
var serverName = server.serverName
if (serverName.startsWith("Main", false)) {
holder.itemView.server_detail_id.typeface = Typeface.DEFAULT_BOLD
}
holder.setText(R.id.server_detail_id, serverName)
}
}
Any suggestions how I could solve this better?
In the constructor for the view holder, you are given the item view. You can do a findViewById() on this view, store the result and refer to it in onBindViewHolder().
On a separate topic, you will want to always set the type face in onBindViewHolder() since the view holders are reused.
I have a recycler view in my layout, at first it will be filled by data which is stored in local database, and then after a few second it will be updated using server.
the problem is when it updates, items of recycler view change suddenly, how can I set an animation for recycler view that change the items smoothly?
I notify my recycler view just like this:
fun add(list: List<BestStockModel>) {
items.clear()
items.addAll(list)
notifyItemRangeChanged(0, list.size)
}
There's a better way for you do so, you can use ListAdapter link.
Using ListAdapter you can simply submit a new list and the adapter will calculate the diff between the old one and the new one and add need animations for new/changed/deleted items.
It can detect the diff using simple callbacks that you provide to it.
Here's an example that you can use as a reference:
class HomeMoviesAdapter : ListAdapter<Movie, MoviesViewHolder>(
//note the following callbacks, ListAdapter uses them
// in order to find diff between the old and new items.
object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem.title == newItem.title //this can be a unique ID for the item
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem == newItem
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MoviesViewHolder {
val v: View = LayoutInflater.from(parent.context)
.inflate(R.layout.movies_item_view, parent, false)
return MoviesViewHolder(v)
}
override fun onBindViewHolder(holder: MoviesViewHolder, position: Int) {
//your binding logic goes here as usual.
}
}
And then from where you have the list (ex: fragment) you can do the following:
adapter.submit(newList)
And that's it for the list adapter to do the needed animations for you.
There's one gotcha though: if submitted the same list reference, the adapter will consider it the same as the old list, meaning it won't trigger the diff calculations. Note the following example:
//the following is a bad practice DO NOT do this!
val list: MutableList<Int> = mutableListOf(1, 2, 3)
adapter.submitList(list)
list.clear()
list.add(7)
adapter.submitList(list) //nothing will happen, since it's the same ref
Compare that to the following:
//the following is good practice, try to do the following!
adapter.submitList(listOf(1, 2, 3))
adapter.submitList(listOf(7)) //will delete all the old items, insert 7 and will also trigger the need animations correctly.
Although they both seem similar, they quite different: the second one submits a totally new list "reference-wise" to the adapter, which will cause the ListAdapter to trigger the calculations correctly.
I am using FirestorePagingAdapter to load data from firestore and display them in recyclerView.
I get the item and bind it to the view.
override fun onBindViewHolder(holder: MyViewHolder, position: Int, model: Question) {
holder.bind(model)
}
fun bind(question: Question) {
//here I load questionData in my viewholder
}
I set onClickListener to textViews in my viewHolder, my question model has up to 5 options which are shown in my viewHolder, so when user presses one I want to update data to the model
override fun onClick(v: View) {
val pressedQuestion = getItem(adapterPosition)?.toObject(Question::class.java)
}
Inside clickListener I get pressed question by calling getItem and toObject.
There I want to update field pressedQuestion.questionId = "answered"
But when this view is scrolled of the screen and than back on sreen data doesn't seem to be changed, questionId field remains the same even though I set it to "answered".
I tried calling every version of notifyDatasetChanged method but it doesn't work.
How can I change data that FirestorePagingAdapter is loading in my viewHolder from inside onClickListener
I'm new in Android dev and now porting my iOS app.
Trying to make pretty complex RecyclerView, but at some moment behavoir of the specific row is duplicated on other row after notifyDataSetChanged() method.
There are three rows with same ViewType in first part of RecyclerView
Each of them has TextView and EditText widgets, that I'm populating in CustomViewHolder class.
First and second rows should work as always: when I'm click in EditText - the keyboard opens. But third row EditText's focus should initiate dialog alert. Everything works great until reload of adapter's DataSet. After DataSet reload the first row's EditText also begins to open the dialog alert instead of normal opening of the keyboard.
Looks like I'm missing something and somehow referencing to the same object when customizing my rows. Here's my adapter code (simplified):
class NewRequestsRecyclerAdapter(val context: Context, val parameters:ArrayList<NewRequestsFragment.ParameterCell>,val delegate:NewRequestProtocol?): RecyclerView.Adapter<NewRequestsRecyclerAdapter.CustomViewHolder>() {
enum class RowType {
Header,Parameter
}
override fun getItemCount(): Int {
// count logic
}
override fun onCreateViewHolder(parent : ViewGroup, viewType: Int): CustomViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val cellForRow = when (RowType.values()[viewType]) {
RowType.Header -> layoutInflater.inflate(R.layout.cell_header,parent,false)
RowType.Parameter -> layoutInflater.inflate(R.layout.cell_parameter_new_requests,parent,false)
}
return CustomViewHolder(cellForRow, RowType.values()[viewType])
}
override fun getItemViewType(position: Int): Int {
// Here's ItemViewType logic ...
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.bindMenu(position)
}
inner class CustomViewHolder(val cellView: View, val type:RowType): RecyclerView.ViewHolder(cellView) {
fun bindMenu(row:Int) {
when (type) {
RowType.Header -> {
val nameView = cellView.findViewById<TextView>(R.id.headerName)
// other logic to populate Header views
}
RowType.Parameter -> {
val nameView = cellView.findViewById<TextView>(R.id.paramName)
val editText = cellView.findViewById<EditText(R.id.paramEditText)
nameView.text = parameters[row-1].name
editText.apply {
hint = parameters[row-1].placeholder
when (parameters[row-1].type) {
NewRequestsFragment.PartsCellType.Name -> {
setText(delegate?.currentItem?.name)
}
NewRequestsFragment.PartsCellType.Number -> {
setText(delegate?.currentItem?.number)
}
NewRequestsFragment.PartsCellType.StateType-> {
setText(delegate?.currentItem?.state)
showSoftInputOnFocus = false
setOnFocusChangeListener { view, changed ->
if (changed) {
inputType = InputType.TYPE_NULL
delegate?.showStateDialog()
}
}
}
}
}
}
I know to resolve that problem I can show alert with button, but I would like to know why my code leads to this behavior.
Could you please guide me what I'm missing?
This type of problem with a RecyclerView where one item mysteriously takes on the attributes or behavior of another item is usually due to not resetting the view holder.
You are defining the behavior of your view holders when they are created, so, the first time, all view holders are created and behave appropriately. When things change, the view holders are reused and not recreated. As a result, things can get mixed up such as getting a dialog opened when the keyboard should show.
To correct this, reset the view holder when it is bound to behave the way you want.