is there any alternate for arraylist.remove(element) function? - android

my recycler view reloads with spinner value, selected data is stored in a global arraylist, and here is my recycler view code to do the selection and deselection. Selection and deselection works just fine, but when i select and change spinner value, and then come to back original spinner value, where items are already selected, deselction happens, but item is not removed from the global arraylist. While debugging i found that cursor reaches there, but i dont know why .remove() isnt working. Is there any alternative for it or am i doing wrong? Is there anything i should know that why isnt the item removed.
.
.
.
class RecyclerViewAdapter(val dataList:ArrayList<ModelClass>,val onItemClicked: (Int) -> Unit):RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
object ob {
val dataSelected = ArrayList<ModelClass>()
val hm = HashMap<ModelClass,String>()
}
fun setData(listModel: List<ModelClass>) {
dataList.clear()
dataList.addAll(listModel)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemViewBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding, parent.context)
}
#SuppressLint("ResourceAsColor")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindItems(dataList[position])
}
override fun getItemCount() = dataList.size
inner class ViewHolder(
val binding: ItemViewBinding,
val context: Context
) : RecyclerView.ViewHolder(binding.root) {
var count=0
#SuppressLint("ResourceAsColor")
fun restore() {
for (i in 0 until ob.dataSelected.size) {
for (j in 0 until dataList.size) {
if (ob.dataSelected[i].sku_code == (dataList[j]).sku_code) {
if (adapterPosition == j) {
itemView.isSelected = true
itemView.setBackgroundColor(R.color.black)
count=count+1
println("****")
}
}
}
}
if(!itemView.isSelected){
itemView.isSelected = false
itemView.setBackgroundResource(0)
}
}
#SuppressLint("ResourceAsColor", "ResourceType")
fun bindItems(data: ModelClass) = with(binding) {
binding.itemQuant.text = data.item_quant
binding.itemName.text = data.item_name
binding.mfgName.text = data.mfg
binding.quantity.text = data.item_stock.toString()
count=0
restore()
itemView.setOnClickListener {
count += 1
var isPresent:Int
if (count%2 == 0){
isPresent=1
}
else
{
isPresent=0
}
if (isPresent == 1) {
it.setBackgroundResource(0) //works
ob.dataSelected.remove(dataList[adapterPosition]) //doesnt work if spinner value is changed and changed back. works while still on same screen.
} else {
if (isPresent == 0) {
it.setBackgroundColor(R.color.black)
ob.dataSelected.add(dataList[adapterPosition])
}
// onItemClicked.invoke(adapterPosition)
}
}
}
}
}

Are you sure it's not being removed? Have you set a breakpoint and checked the contents of the array (or just logged it)? Because it looks like when you're binding a viewholder, you just check if the item is in dataSelected, and if it is you set the view's selected and backgroundColor values. It doesn't look like you change them back if it's not in dataSelected, so that selected appearance "sticks".
It looks like you're trying to reset them with this:
if(!itemView.isSelected){
itemView.isSelected = false
itemView.setBackgroundResource(0)
}
but that only works if itemView.selected hasn't been set to true, which it has if your item is in dataSelected. You need to do it like this:
for (i in 0 until ob.dataSelected.size) {
for (j in 0 until dataList.size) {
// rolling multiple conditions into a single value makes it easier to do an if/else
val selected = ob.dataSelected[i].sku_code == (dataList[j]).sku_code && adapterPosition == j
if (selected) {
itemView.isSelected = true
itemView.setBackgroundColor(R.color.black)
count=count+1
println("****")
} else {
itemView.isSelected = false
itemView.setBackgroundResource(0)
}
}
}
That way you're always updating the contents of the view holder to reflect the state of the current item. That's especially important in RecyclerViews, because the point of those is that the ViewHolders get reused to display different items, and any View attributes you don't set when binding (e.g. colours, checkbox statuses) will just stay on whatever they were last set to when displaying some other item.
btw, you can simplify that looping situation by just checking if anything in dataSelected has the SKU you're looking for:
val itemSku = datalist[adapterPosition].sku_code
val selected = ob.dataSelected.any { it.sku_code == itemSku }
ideally you wouldn't be using adapterPosition to find the current item either - the item you're matching is passed in to bindItems (as data), you could just pass that to your restore function
edit if the items aren't being removed from dataSelected, at a guess it's because the object you're fetching from dataList "doesn't exist" in dataSelected. By "doesn't exist" I mean there isn't an object that equals the one you're trying to remove.
ob.dataSelected.remove(dataList[adapterPosition])
I'm assuming that might be the case because you're not doing straight object comparisons when you're checking if an item is in the selected array, you're specifically comparing their sku_code values instead
if (ob.dataSelected[i].sku_code == (dataList[j]).sku_code)
If you can't just do if (obj.dataSelected[i] == dataList[j]), then remove won't work either - it's the same check. So maybe you need to look into making those objects equal (e.g. using data classes if that works for what you're doing), or use removeAll with a predicate (remove only works with a specific item):
ob.dataSelected.removeAll { it.sku_code == dataList[adapterPosition].sku_code }
You'll still have the problem of the UI not being updated though, like the first part of the answer explains! So you might have two problems here

change this line
ob.dataSelected.add(dataList[adapterPosition])
to this
ob.dataSelected.add(dataList[adapterPosition].copy)
Don't forget to call notfiyDatasetChanged() after that

Related

ViewPager (RecyclerView) with custom item count doesn't update data properly

I've created an adapter (extending ListAdapter with DiffUtil.ItemCallback) for my RecyclerView. It's an ordinary adapter with several itemViewTypes, but it should be smth like cyclic, if API sends flag and dataset size is > 1 (made by overriding getItemCount() to return 1000 when conditions == true).
When I change app locale through app settings, my fragment recreates, data loads asynchronously (reactively, several times in a row, from different requests, depending on several rx fields, which causes data set to be a combination of data on different languages just after locale is changed (in the end all dataset is correctly translated btw) (make it more like synchronous is not possible because of feature specifics)), posting its values to LiveData, which triggers updates of recycler view, the problem appears:
After last data set update some of the views (nearest to currently displayed and currently displayed) appear not to be translated.
Final data set, which is posted to LiveData is translated correctly, it even has correct locale tag in its id. Also after views are recycled and we return back to them - they are also correct.
DiffUtil is computed correctly also (I've tried to return only false in item callbacks and recycler view still didn't update its view holders correctly).
When itemCount == list.size everything works fine.
When adapter is pretending to be cyclic and itemCount == 1000 - no.
Can somebody explain this behaviour and help to figure out how to solve this?
Adapter Code Sample:
private const val TYPE_0 = 0
private const val TYPE_1 = 1
class CyclicAdapter(
val onClickedCallback: (id: String) -> Unit,
val onCloseClickedCallback: (id: String) -> Unit,
) : ListAdapter<IViewData, RecyclerView.ViewHolder>(DataDiffCallback()) {
var isCyclic: Boolean = false
set(value) {
if (field != value) {
field = value
}
}
override fun getItemCount(): Int {
return if (isCyclic) {
AdapterUtils.MAX_ITEMS // 1000
} else {
currentList.size
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_0 -> Type0.from(parent)
TYPE_1 -> Type1.from(parent)
else -> throw ClassCastException("View Holder for ${viewType} is not specified")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is Type0 -> {
val item = getItem(
AdapterUtils.actualPosition(
position,
currentList.size
)
) as ViewData.Type0
holder.setData(item, onClickedCallback)
}
is Type1 -> {
val item = getItem(
AdapterUtils.actualPosition(
position,
currentList.size
)
) as ViewData.Type1
holder.setData(item, onClickedCallback, onCloseClickedCallback)
}
}
}
override fun getItemViewType(position: Int): Int {
return when (val item = getItem(AdapterUtils.actualPosition(position, currentList.size))) {
is ViewData.Type0 -> TYPE_0
is ViewData.Type1 -> TYPE_1
else -> throw ClassCastException("View Type for ${item.javaClass} is not specified")
}
}
class Type0 private constructor(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun setData(
viewData: ViewData.Type0,
onClickedCallback: (id: String) -> Unit
) {
(itemView as Type0View).apply {
acceptData(viewData)
setOnClickedCallback { url ->
onClickedCallback(viewData.id,)
}
}
}
companion object {
fun from(parent: ViewGroup): Type0 {
val view = Type0View(parent.context).apply {
layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
return Type0(view)
}
}
}
class Type1 private constructor(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun setData(
viewData: ViewData.Type1,
onClickedCallback: (id: String) -> Unit,
onCloseClickedCallback: (id: String) -> Unit
) {
(itemView as Type1View).apply {
acceptData(viewData)
setOnClickedCallback { url ->
onClickedCallback(viewData.id)
}
setOnCloseClickedCallback(onCloseClickedCallback)
}
}
companion object {
fun from(parent: ViewGroup): Type1 {
val view = Type1View(parent.context).apply {
layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
return Type1(view)
}
}
}
}
ViewPager Code Sample:
class CyclicViewPager #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
ICyclicViewPager {
private val cyclicViewPager: ViewPager2
private lateinit var onClickedCallback: (id: String) -> Unit
private lateinit var onCloseClickedCallback: (id: String) -> Unit
private lateinit var adapter: CyclicAdapter
init {
LayoutInflater
.from(context)
.inflate(R.layout.v_cyclic_view_pager, this, true)
cyclicViewPager = findViewById(R.id.cyclic_view_pager)
(cyclicViewPager.getChildAt(0) as RecyclerView).apply {
addItemDecoration(SpacingDecorator().apply {
dpBetweenItems = 12
})
clipToPadding = false
clipChildren = false
overScrollMode = RecyclerView.OVER_SCROLL_NEVER
}
cyclicViewPager.offscreenPageLimit = 3
}
override fun initialize(
onClickedCallback: (id: String) -> Unit,
onCloseClickedCallback: (id: String) -> Unit
) {
this.onClickedCallback = onClickedCallback
this.onCloseClickedCallback = onCloseClickedCallback
adapter = CyclicAdapter(
onClickedCallback,
onCloseClickedCallback,
).apply {
stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
}
cyclicViewPager.adapter = adapter
}
override fun setState(viewPagerState: CyclicViewPagerState) {
when (viewPagerState.cyclicityState) {
is CyclicViewPagerState.CyclicityState.Enabled -> {
adapter.submitList(viewPagerState.pages) {
adapter.isCyclic = true
cyclicViewPager.post {
cyclicViewPager.setCurrentItem(
// Setting view pager item to +- 500
AdapterUtils.getCyclicInitialPosition(
adapter.currentList.size
), false
)
}
}
}
is CyclicViewPagerState.CyclicityState.Disabled -> {
if (viewPagerState.pages.size == 1 && adapter.isCyclic) {
cyclicViewPager.setCurrentItem(0, false)
adapter.isCyclic = false
}
adapter.submitList(viewPagerState.pages)
}
}
}
}
Adapter Utils Code:
object AdapterUtils {
const val MAX_ITEMS = 1000
fun actualPosition(position: Int, listSize: Int): Int {
return if (listSize == 0) {
0
} else {
(position + listSize) % listSize
}
}
fun getCyclicInitialPosition(listSize: Int): Int {
return if (listSize > 0) {
MAX_ITEMS / 2 - ((MAX_ITEMS / 2) % listSize)
} else {
0
}
}
}
Have tried not to use default itemView variable of RecyclerView (became even worse).
Tried to make diff utils always return false, to check if it calculates diff correctly (yes, correctly)
Tried to add locale tags to ids of data set items (didn't help to solve)
Tried to post empty dataset on locale change before setting new data to it (shame on me, shouldn't even think about it)
Tried do add debounce to rx to make it wait a bit before update (didn't help)
UPD: When I call adapter.notifyDatasetChanged() manually, which is not the preferred way, everything works fine, so the question is why ListAdapter doesn't dispatch notify callbacks properly in my case?
The issue with ListAdapter is that it doesn't clearly state that you need to supply a new list for it to function.
In other words, the documentation says: (and I quote the source code):
/**
* Submits a new list to be diffed, and displayed.
* <p>
* If a list is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
*
* #param list The new list to be displayed.
*/
public void submitList(#Nullable List<T> list) {
mDiffer.submitList(list);
}
The key word being new list.
However, as you can see there, all the adapter does is defer to the DiffUtil and calls submitList there.
So when you look at the actual source code of the AsyncListDiffer you will notice it does, at the beginning of its code block:
if (newList == mList) {
// nothing to do (Note - still had to inc generation, since may have ongoing work)
if (commitCallback != null) {
commitCallback.run();
}
return;
}
In other words, if the new list (reference) is the same as the old one, regardless of their contents, don't do anything.
This may sound cool but it means that if you have this code, the adapter will not really update:
(pseudo...)
var list1 = mutableListOf(...)
adapter.submitList(list1)
list1.add(...)
adapter.submitList(list1)
The reason is list1 is the same reference your adapter has, so the differ exits prematurely, and doesn't dispatch any changes to the adapter.
Quite obscure, I know.
The solution, as pointed in many SO answers is to create a copy of the list itself.
Most users do
var list1 = mutableListOf(...)
adapter.submitList(list1)
var list2 = list1.toMutableList()
list2.add(...)
adapter.submitList(list2)
The call to toMutableList() creates a new list containing the items of list1 and so the comparison above if (newList == mList) { should now be false and the normal code should execute.
UPDATE
Keep in mind that a lot of developers make the mistake of...
var list = mutableListOf...
adapter.submitList(list)
list.add(xxx)
adapter.submitList(list.toList())
This doesn't work, because the new list you create, is referencing the same objects the adapter has. This means that both lists list and list.toList() are pointing to the same things despite being two instances of an ArrayList.
But the side-effect is that DiffUtil compares the items and they are the same, so no diff is dispatched to the adapter either.
The correct sequence is...
val list = mutableListOf(...)
adapter.submitList(list.toList())
// Make a copy first, so we can alter it as we please without the *current list held by the adapter* from being affected.
var modified = list.toMutableList()
modified.add(...)
adapter.submitList(modified)
After taking a look at your sample in GitHub, I was able to reproduce the issue. With only about 30-40 minutes of playing with it, I can say that I'm not 100% sure what component is not updating.
Things I've noticed.
The onBindViewHolder method is not called when you change the locale (except maybe the 1st time?).
I do not understand why the need to post to the adapter after you've submitted the list in the callback:
cyclicViewPager.setCurrentItem(
// Setting view pager item to +- 500
AdapterUtils.getCyclicInitialPosition(
adapter.currentList.size
), false
)
Why ? This means the user loses their current position.
Why not keep the existing?
I noticed you do cyclicViewPager.offscreenPageLimit = 3 this effectively disables the RecyclerView "logic" for handling changes, and uses instead the usual ViewPager state adapter logic of "prefetching/keeping" N (3 in your case) pages in "advance".
At first I thought this was causing issues, but removing it (which sets it to -1 which is the default and the "use RecyclerView" value, didn't make a big change (though I did notice some changes here and there, as in it would sometimes update the current one -but not the next ones within 2~3 pages).
The documentation says:
Set the number of pages that should be retained to either side of the currently visible page(s). Pages beyond this limit will be recreated from the adapter when needed. Set this to OFFSCREEN_PAGE_LIMIT_DEFAULT to use RecyclerView's caching strategy.
So I would have imagined that the default value would be aided by the ListAdapter and its DiffUtil. Doesn't seem to be the case.
What I did try (among a few other things) was to see if the issue was in the actual adapter (or at least the viewPager dependency on its adapter). I ran out of time (work!) but I noticed that if you do:
override fun setState(viewPagerState: CyclicViewPagerState) {
when (viewPagerState.cyclicityState) {
is CyclicViewPagerState.CyclicityState.Enabled -> {
// call initialize again, to recreate the adapter
initialize(this.onClickedCallback, this.onCloseClickedCallback)
adapter.submitList(viewPagerState.pages) {
adapter.isCyclic = true
// Setting vp item to ... (code omitted for brevity)
}
This works. It's theoretically less efficient as you're recreating the whole adapter, but in your example you're effectively creating an ENTIRE new set of data changing every ID, so in terms of performance, I'd argue this is more efficient as there's no need to recalculate changes and dispatch them, since to the eyes of the Diff Util, all the rows are different. By recreating the adapter, well... the VP has to reinit anyway.
I noticed this worked fine in your example.
I went ahead and added two more things, because the "silly" adapter cannot reliably tell you which position is the current... you can naively save it:
In CyclicViewPager:
var currentPos: Int = 0
init {
...
this.cyclicViewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int)
currentPos = position
}
})
}
And then
is CyclicViewPagerState.CyclicityState.Enabled -> {
initialize(this.onClickedCallback, this.onCloseClickedCallback)
adapter.submitList(viewPagerState.pages) {
adapter.isCyclic = true
if (adapter.currentList.size <= currentPos) {
cyclicViewPager.setCurrentItem(currentPos, false)
} else {
cyclicViewPager.setCurrentItem(
// Setting view pager item to +- 500
AdapterUtils.getCyclicInitialPosition(
adapter.currentList.size
), false
)
}
}
}
This does work, but of course, you're recreating the entire VP adapter again, so it may not be desired.
At this point, I'd either need to spend much more time trying to figure out which part of VP, RV, or its dependencies is not "dispatching" the correct data. My guess would be somewhere around some silly ViewPager optimization combined with Android terribly unreliable View system, not picking a message in the queue; but I may be also terribly wrong ;)
I hope someone smarter and/or with more coffee in their system can find out a simpler solution.
(all in all, I found the sample project relatively easy to navigate, but the design of your data a bit convoluted, but... as it was a sample, it's hard to tell what "real-life" data structures you really have).

Moving item in RecyclerView doubles item in view

I'm building a contact list (ArrayList) in a messaging app using RecyclerView.
When a contact updates, I want to move that user to the top of the list, while all other items in the list move one step down.
I'm using Firestore to get the list of users.
DocumentChange.Type.MODIFIED -> {
val matchThatChanged = dc.newIndex
matchesArrayList[matchThatChanged] = ChatMatchListMatch(
matchUserID)
adapter.notifyItemChanged(matchThatChanged) //Ensures change is visible immediately
val fromPosition = matchesArrayList.indexOfFirst {
it!!.matchUserID == matchUserID
}
Log.d(TAG, "From position A: $matchThatChanged")
if (fromPosition != 0) {
adapter.moveMatchToTop(
fromPosition, ChatMatchListMatch(
matchUserID
)
)
}
}
In the Log here it correctly outputs 1 when I make the first move. However, thereafter it outputs 0 when I try to update the other user? This is not correct. The user that gets pushed down should no longer be at 0, it should be at 1. Because it is now wrongly at 0, it is not running the code (this is not the issue. The issue is that is should not be 0 in the first place).
Here's the code in my adapter:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.chat_matchlist_item, parent, false)
return CustomViewHolder(view)
}
// Passes the ContactListMatch object to a ViewHolder so that the contents can be bound to UI.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val contactListMatch = mMatchesArrayList[position]
(holder as CustomViewHolder).bind(contactListMatch)
}
fun moveMatchToTop(fromPosition: Int, chatMatchListMatch: ChatMatchListMatch) {
mMatchesArrayList.removeAt(fromPosition)
notifyItemChanged(fromPosition)
notifyItemRemoved(fromPosition)
mMatchesArrayList.add(0, chatMatchListMatch)
notifyItemInserted(0)
notifyItemChanged(0)
notifyItemChanged(1)
}
The list I have to begin with is:
When I update only the bottom user (2), it displays correctly (moving user two to the top and the other user down):
I now try to update original user 1 again, and I expect it to go on top again, like this:
But instead I get this:
I figured out the solution. I had to make several changes, but I'll try to make this helpful to others who may struggle with similar issues:
I used an index in Firestore and used two "orderby" queries to get the list in the order I wanted:
myCollectionRef
.orderBy("unread", Query.Direction.DESCENDING)
.orderBy("timeStamp", Query.Direction.DESCENDING)
.addSnapshotListener
Then under case MODIFIED:
DocumentChange.Type.MODIFIED -> {
I had to get the old index and the new index of the item changed. When I get the old index (by iterating through my list to look for it with a for loop), I ensure to remove the item from the list and notify my adapter that I removed the item:
val newIndex = dc.newIndex
var oldIndex = -1
for (i in myList.indices.reversed()) {
if (myList[i]!!.IDnumber == userID) {
oldIndex = i
myList.removeAt(oldIndex)
adapter.notifyItemRemoved(oldIndex)
}
}
Then I have to add the changed item back into my list in the correct position. Since I have the new index, this is easy. I also notify my adapter of the change:
myList.add(
newIndex, MySpecialItem(
userID!!,
userImageOrWhatever!!
)
)
adapter.notifyItemInserted(newIndex)
and that's it.

RecyclerView sets wrong MotionLayout state for its items

First: I created a sample project showing this problem. By now I begin to think that this is a bug in either RecyclerView or MotionLayout.
https://github.com/muetzenflo/SampleRecyclerView
This project is set up a little bit different than what is described below: It uses data binding to toggle between the MotionLayout states. But the outcome is the same. Just play around with toggling the state and swiping between the items. Sooner than later you'll come upon a ViewHolder with the wrong MotionLayout state.
So the main problem is:
ViewHolders outside of the screen are not updated correctly when transition from one MotionLayout state to another.
So here is the problem / What I've found so far:
I am using a RecyclerView.
It has only 1 item type which is a MotionLayout (so every item of the RV is a MotionLayout).
This MotionLayout has 2 states, let's call them State big and State small
All items should always have the same State. So whenever the state is switched for example from big => small then ALL items should be in small from then on.
But what happens is that the state changes to small and most(!) of the items are also updated correctly. But one or two items are always left with the old State. I am pretty sure it has to do with recycled ViewHolders. These steps produce the issue reliably when using the adapter code below (not in the sample project):
swipe from item 1 to the right to item 2
change from big to small
change back from small to big
swipe from item 2 to the left to item 1
=> item 1 is now in the small state, but should be in the big state
Additional findings:
After step 4 if I continue swiping to the left, there comes 1 more item in the small state (probably the recycled ViewHolder from step 4). After that no other item is wrong.
Starting from step 4, I continue swiping for a few items (let's say 10) and then swipe all the way back, no item is in the wrong small state anymore. The faulty recycled ViewHolder seems to be corrected then.
What did I try?
I tried to call notifyDataSetChanged() whenever the transition has completed
I tried keeping a local Set of created ViewHolders to call the transition on them directly
I tried to use data-binding to set the motionProgress to the MotionLayout
I tried to set viewHolder.isRecycable(true|false) to block recycling during the transition
I searched this great in-depth article about RVs for hint what to try next
Anyone had this problem and found a good solution?
Just to avoid confusion: big and small does not indicate that I want to collapse or expand each item! It is just a name for different arrangement of the motionlayouts' children.
class MatchCardAdapter() : DataBindingAdapter<Match>(DiffCallback, clickListener) {
private val viewHolders = ArrayList<RecyclerView.ViewHolder>()
private var direction = Direction.UNDEFINED
fun setMotionProgress(direction: MatchCardViewModel.Direction) {
if (this.direction == direction) return
this.direction = direction
viewHolders.forEach {
updateItemView(it)
}
}
private fun updateItemView(viewHolder: RecyclerView.ViewHolder) {
if (viewHolder.adapterPosition >= 0) {
val motionLayout = viewHolder.itemView as MotionLayout
when (direction) {
Direction.TO_END -> motionLayout.transitionToEnd()
Direction.TO_START -> motionLayout.transitionToStart()
Direction.UNDEFINED -> motionLayout.transitionToStart()
}
}
}
override fun onBindViewHolder(holder: DataBindingViewHolder<Match>, position: Int) {
val item = getItem(position)
holder.bind(item, clickListener)
val itemView = holder.itemView
if (itemView is MotionLayout) {
if (!viewHolders.contains(holder)) {
viewHolders.add(holder)
}
updateItemView(holder)
}
}
override fun onViewRecycled(holder: DataBindingViewHolder<Match>) {
if (holder.adapterPosition >= 0 && viewHolders.contains(holder)) {
viewHolders.remove(holder)
}
super.onViewRecycled(holder)
}
}
I made some progress but this is not a final solution, it has a few quirks to polish. Like the animation from end to start doesn't work properly, it just jumps to the final position.
https://github.com/fmatosqg/SampleRecyclerView/commit/907ec696a96bb4a817df20c78ebd5cb2156c8424
Some things that I changed but are not relevant to the solution, but help with finding the problem:
made duration 1sec
more items in recycler view
recyclerView.setItemViewCacheSize(0) to try to keep as few unseen items as possible, although if you track it closely you know they tend to stick around
eliminated data binding for handling transitions. Because I don't trust it in view holders in general, I could never make them work without a bad side-effect
upgraded constraint library with implementation "androidx.constraintlayout:constraintlayout:2.0.0-rc1"
Going into details about what made it work better:
all calls to motion layout are done in a post manner
// https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
fun safeRunBlock(block: () -> Unit) {
if (ViewCompat.isLaidOut(motionLayout)) {
block()
} else {
motionLayout.post(block)
}
}
Compared actual vs desired properties
val goalProgress =
if (currentState) 1f
else 0f
val desiredState =
if (currentState) motionLayout.startState
else motionLayout.endState
safeRunBlock {
startTransition(currentState)
}
if (motionLayout.progress != goalProgress) {
if (motionLayout.currentState != desiredState) {
safeRunBlock {
startTransition(currentState)
}
}
}
This would be the full class of the partial solution
class DataBindingViewHolder<T>(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
val motionLayout: MotionLayout =
binding.root.findViewById<MotionLayout>(R.id.root_item_recycler_view)
.also {
it.setTransitionDuration(1_000)
it.setDebugMode(DEBUG_SHOW_PROGRESS or DEBUG_SHOW_PATH)
}
var lastPosition: Int = -1
fun bind(item: T, position: Int, layoutState: Boolean) {
if (position != lastPosition)
Log.i(
"OnBind",
"Position=$position lastPosition=$lastPosition - $layoutState "
)
lastPosition = position
setMotionLayoutState(layoutState)
binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
// https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
fun safeRunBlock(block: () -> Unit) {
if (ViewCompat.isLaidOut(motionLayout)) {
block()
} else {
motionLayout.post(block)
}
}
fun setMotionLayoutState(currentState: Boolean) {
val goalProgress =
if (currentState) 1f
else 0f
safeRunBlock {
startTransition(currentState)
}
if (motionLayout.progress != goalProgress) {
val desiredState =
if (currentState) motionLayout.startState
else motionLayout.endState
if (motionLayout.currentState != desiredState) {
Log.i("Pprogress", "Desired doesn't match at position $lastPosition")
safeRunBlock {
startTransition(currentState)
}
}
}
}
fun startTransition(currentState: Boolean) {
if (currentState) {
motionLayout.transitionToStart()
} else {
motionLayout.transitionToEnd()
}
}
}
Edit: added constraint layout version

Listener of a RecyclerView being apply to others RecyclersView

Well, I'm working with Kotling right now, and I've a listener in onBindViewHolder inside the ViewHolderAdapter, but, the behavior of this listener is being applied at each 10 positions. The behavior is just to change a image color using a color filter when I touch that image. Pretty simple.
I've tried to use holder.setIsRecyclable(false) but when the Recycler get out of the screen, the image gets to it's normal design, and not more red.
Here's my ViewHolderAdapter
class ViewHolderAdapter(private var products:List<EachProduct>):RecyclerView.Adapter<ViewHolderAdapter.ViewHolder>(){
override fun onCreateViewHolder(parent:ViewGroup,viewType:Int):ViewHolder{
val view=LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_layout,parent,false)
return ViewHolder(view)
}
override fun getItemCount():Int{
return products.size
}
override fun onBindViewHolder(holder:ViewHolder,position:Int){
//holder.setIsRecyclable(false)
holder.likeImage.setOnClickListener{
holder.likeImage.setColorFilter(Color.RED)
}
}
class ViewHolder(itemView:View):RecyclerView.ViewHolder(itemView){
var likeImage:ImageView=itemView.LikeImage
}
}
I expected that only the image of the touched RecyclerView change, not, for example, at every 10 rows. Can someone provide me an example on how to set a listener inside a image in a RecyclerView?
ImageView.setColorFilter() assigns a ColorFilter object to a field of the ImageView. This means that when your ViewHolder is recycled and re-bound to a new image, the ColorFilter is still in place and will apply to whatever image is loaded in.
Additionally, there's no guarantee that the same ViewHolder will be used for the same position if you scroll up and down.
What you need to do is somehow store inside your data set whether or not the image should have a filter applied to it. For example, maybe you store the last clicked position:
var lastClickedPosition = -1
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (position == lastClickedPosition) {
holder.likeImage.setColorFilter(Color.RED)
} else {
holder.likeImage.setColorFilter(null)
}
holder.likeImage.setOnClickListener {
val previousPosition = lastClickedPosition
lastClickedPosition = position
notifyItemChanged(position)
if (previousPosition != -1) {
notifyItemChanged(previousPosition)
}
}
}
Or maybe you want to be able to apply the filter to multiple rows at once. Then perhaps you could add a field to whatever list backs your adapter, or create a mutable set of highlighted positions:
val clickedPositions = mutableSetOf<Int>()
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (clickedPositions.contains(position)) {
holder.likeImage.setColorFilter(Color.RED)
} else {
holder.likeImage.setColorFilter(null)
}
holder.likeImage.setOnClickListener {
clickedPositions.add(position)
notifyItemChanged(position)
}
}

How can I shuffle the contents of a RealmResults object

I have a RealmResults object which holds output of a .where.findAll() query. I am showing contents of this object in a RecyclerView, but since the contents of the RealmResults object are static, it shows same set of records evertime the RecyclerView is opened. I want to shuffle (randomize) the contents of RealmResults so that I can show different values that are in the RealmResults in my RecyclerView. Please suggest some possible ways in which I can perform the same.
You should not randomize the list itself, you should randomize your access of it (the indices).
Build a list that contains the indexes from [0, n-1]
List<Integer> indices = new ArrayList<>(realmResults.size());
for(int i = 0; i < realmResults.size(); i++) {
indices.add(i);
}
Shuffle it
Collections.shuffle(indices);
Then when you pick the view holder data, index realm results with indexed random position
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof YourHolder) {
YourHolder yourHolder = (YourHolder) holder;
RealmData realmData = realmResults.get(indices.get(position));
//init data for your holder
}
}
For future references, when extending RealmRecyclerViewAdapter, if you enabled the autoUpdate feature, the above solution will not be enough in cases of updates.
Here is a solution based on the same idea, handling the updates.
Here I ignore the update type and just reset the whole shuffling state if the item count changes. It's not hard to adapt for more specific events (itemRangeAdded, itemRangeRemoved, ...) you just need to update indicesMapList and displayedMapList accordingly.
This adapter handles shuffling/sorting and is ready to handle filtering as well (it was not asked but pulled this from a code I did, you can remove unnecessary part if not wanted).
(To filter just update the displayedMapList with indices of items you want to display, sort/shuffle operations will preserve the filter)
abstract class BaseRealmAdapter<T : RealmObject, S : RecyclerView.ViewHolder>(data: OrderedRealmCollection<T>,
autoUpdate: Boolean) :
RealmRecyclerViewAdapter<T, S>(data, autoUpdate) {
private val indicesMapList = arrayListOf(*(0 until getRealItemCount()).toList().toTypedArray())
val displayedMapList = ArrayList(indicesMapList)
var shuffled = false
private set
/** Displayed item count (after possible filter) */
override fun getItemCount() = displayedMapList.size
override fun getItem(index: Int) = super.getItem(displayedMapList[index])
/** Unfiltered item count */
fun getRealItemCount() = super.getItemCount()
fun shuffle() {
if (getRealItemCount() == 0)
return
shuffled = true
Collections.shuffle(indicesMapList)
// Keep same order
displayedMapList.sortBy { indicesMapList.indexOf(it) }
notifyDataSetChanged()
}
fun sort() {
if (getRealItemCount() == 0)
return
shuffled = false
indicesMapList.sort()
displayedMapList.sort()
notifyDataSetChanged()
}
protected fun resetIndicesMap(notifyDataSetChanged: Boolean = true) {
shuffled = false
indicesMapList.clear()
indicesMapList.addAll(0 until getRealItemCount())
resetDisplayedList()
if (notifyDataSetChanged)
notifyDataSetChanged()
}
protected fun resetDisplayedList() {
displayedMapList.clear()
displayedMapList.addAll(indicesMapList)
}
protected fun getIndicesMapList() = ArrayList(indicesMapList)
override fun onAttachedToRecyclerView(recyclerView: RecyclerView?) {
registerAdapterDataObserver(DataObserver())
super.onAttachedToRecyclerView(recyclerView)
}
/** This implementation will cancel any current shuffled state when adding items */
inner class DataObserver : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
// Item(s) added or removed: this will cancel filtering
if (getRealItemCount() != indicesMapList.size)
resetIndicesMap(true)
}
}
}

Categories

Resources