Android - Drag and Drop on ConcatAdapter - android

Im trying to implement a drag and drop in a ConcatAdapter. I did a ConcatAdapter bc I have multiples sections where the items are different, so I just need to drag and drop inside one section.
I did a ConcatAdapter with 2 adapters
I did the ItemTouchHelper for the recycler view -> itemTouchHelper.attachToRecyclerView(binding.recyclerView)
I guess that it's something related with the ItemTouchHelper bc its set on the recycler view and not on the adapter.
Is there any way I can set just the drag and drop for 1 adapter inside the ConcatAdapter?

Inside onMove from ItemTouchHelper.Callback, you can just decide to ignore some of the moves. Use viewHolder (the ViewHolder which is being dragged) and targetViewHolder and check their bindingAdapter. Or if they're instances of the same adapter, you can use a field that has been set on adapter initialization, to determine which adapter they are.
In the example below I have two adapters, both of type TaskAdapter, but second one holds completed tasks. Uncompleted tasks can't be moved between completed ones and completed tasks can't be moved at all. So if targetViewHolder is bound by the adapter flagged as completed, I just return false and ignore the move. Obviously an item has been dragged over that target item but since it's not actually moving, user understands this isn't allowed and will give up. I think it's acceptable and better than having multiple RecyclerViews.
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
targetViewHolder: RecyclerView.ViewHolder
): Boolean {
val targetAdapter = targetViewHolder.bindingAdapter as TaskAdapter
if (targetAdapter.completed) return false
val adapter = viewHolder.bindingAdapter as TaskAdapter
adapter.moveItem(
viewHolder.bindingAdapterPosition,
targetViewHolder.bindingAdapterPosition
)
return true
}
If you want some items can't be dragged in the first place, you can just return 0 from getDragDirs. In this example they are completed tasks.
override fun getDragDirs(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val adapter = viewHolder.bindingAdapter as TaskAdapter
if (adapter.completed) return 0
return super.getDragDirs(recyclerView, viewHolder)
}

Related

RecyclerView ItemAnimator change order of moving items in default animation

I have a recyclerview that load a list of cards each one with a favorite button. When user touch that button I change the order of my list items setting to first position the new favorite card. That change displays the default animation when NotifyItemMoved is called but I want to display in front the item that is moving up. The default animation displays the item that is moving to botton in front of the rest of the items.
Searching a bit I found that I can implement a custom ItemAnimator and use something like this:
override fun animateMove(
holder: RecyclerView.ViewHolder?,
fromX: Int,
fromY: Int,
toX: Int,
toY: Int
): Boolean {
if ( fromY > toY) {
holder?.itemView?.bringToFront()
}
return super.animateMove(holder, fromX, fromY, toX, toY)
}
It looks that works well but when I scroll the list it crashes with this error: java.lang.RuntimeException: trying to unhide a view that was not hiddenandroidx.constraintlayout.widget.ConstraintLayout
If I remove holder?.itemView?.bringToFront() the default animation runs well again without any crash
The reason why you are getting the "trying to unhide a view that was not hidden" error is because the bringToFront() method of View can interfere with the RecyclerView's internal view recycling mechanism. When a view is recycled, it is hidden and re-used for another item in the list. By calling bringToFront() on a view, you are potentially interfering with this process and causing the view to be incorrectly re-used for a different item.
One way to avoid this issue is to create a custom RecyclerView.ItemAnimator that overrides the animateChange() method to bring the moved view to the front, but does not interfere with the view recycling process. Here's an example implementation:
class CustomItemAnimator : DefaultItemAnimator() {
override fun animateChange(
oldHolder: RecyclerView.ViewHolder?,
newHolder: RecyclerView.ViewHolder?,
preInfo: ItemHolderInfo?,
postInfo: ItemHolderInfo?
): Boolean {
if (oldHolder != null && newHolder != null) {
// Check if the item is being moved up (i.e. to a lower index)
if (oldHolder.adapterPosition > newHolder.adapterPosition) {
// Bring the new holder's item view to the front
newHolder.itemView.bringToFront()
}
}
return super.animateChange(oldHolder, newHolder, preInfo, postInfo)
}
}
This implementation only brings the new holder's item view to the front when the item is being moved up (i.e., to a lower index). This should allow the default RecyclerView view recycling mechanism to work correctly.
To use this custom ItemAnimator, set it on your RecyclerView using the setItemAnimator() method:
recyclerView.setItemAnimator(CustomItemAnimator())
Note: This implementation assumes that each item in the RecyclerView is represented by a single view in the ViewHolder. If you have complex item layouts with multiple views, you may need to modify this implementation to ensure that only the item view is brought to the front.

Update Recycler View Child with any action on another child

I am struggling with one feature that I need to implement in recycler view, I need to update few child based on the action performed on any child.
E.g. I have recycler view with lets say 10 items
Child 1
Child 2
Child 3
-
-
Child 10
Now at a time there are only 5 child that are visible on the screen and rest of them comes only when the list is scrolled. What I wanted to achieve is when I click on child 1 and perform an action, that action returns me to update random Child 4, Child 7 & Child 8
Now the problem is how do I update the child which isn't visible in the list.
I have tried using following solution:-
val childCount = rvQuestion.childCount
for (i in 0 until childCount) {
val child = rvQuestion.getChildAt(i)
.findViewById<TextInputEditText>(R.id.etQuestion)
val questionHint = child.hint
// list is the list of child that needs to be populated with a hint
if(list.contains(questionHint)) {
child.setText("someValue")
}
}
The issue is recycler view never gives the childCount as 10 it gives only 5, which are currently visible and hence the wrong child are getting updated.
Any other way to handle this?
You need to use your RecyclerView adapter to grab the child count and to update any children. The adapter requires to override a getItemCount function which should return the total items in the list. Attempting to get the child count or updating a child directly from the RecyclerView is not the correct way.
class TestAdapter(private val data: MutableList<String>) : RecyclerView.Adapter<TestAdapter.ViewHolder>() {
...
override fun getItemCount(): Int {
return data.size
}
...
}
And to update a child in the your RecyclerView you need to tell the adapter to do that as well. Inside your adapter you can create a method that takes in an index any other necessary data to update a child.
class TestAdapter(private val data: MutableList<String>) : RecyclerView.Adapter<TestAdapter.ViewHolder>() {
...
fun updateChildAt(idx: Int, newValue: String) {
data[idx] = newValue
notifyItemChanged(idx)
}
...
}

RecyclerView's row item views hide/show getting messed up on scroll of the Recyclerview

I am using a RecyclerView to show list of products in my app, I need to group the product based on aisle. while the data are fetched for the first time in the list, the products are grouped correctly with respect to aisle. When we scroll the view, the aisle group divider is shown for the wrong item and the divider gets restored to correct position once the onBindViewHolder gets refreshed automatically.
MyAdapter.class
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
itemsGrouping(pickItem, pickItemView, holder.adapterPosition)
}
private fun itemsGrouping(pickItem: PickItem, pickItemView: View, adapterPosition: Int) {
//Based on some condition
if(SomeCondition)
itemDivider(pickItemView,true)
else
itemDivider(pickItemView,false)
}
private fun itemDivider(v: View, boolean: Boolean) {
if(boolean) {
v.visibility = View.VISIBLE
} else {
v.visibility = View.GONE
}
}
Well, you should know that the view holders are reused in the RecyclerView, so it's probable not the right idea to try to determine the visibility of the divider in onBindViewHolder. I would recommend using item decorator for dividers. Here's the question and answer for that
How to add dividers and spaces between items in RecyclerView?
The problem is RecyclerView recycles previous views in order to be efficient.
I guess "SomeCondition" contains artifacts which are from previous holders.
So at
itemsGrouping(pickItem, pickItemView, holder.adapterPosition)
you should get pickItem and pickItemView from newly bound holder. You should use like
pickItemView = holder.findViewById(R.id.pickItemView);
Or consider using DataBinding Library
Here is a good example (it's in Kotlin) : DataBoundListAdapter
Once you extend your adapter to DataBoundListAdapter and override bind() method, everything inside bind is executed for every row, so you won't get repeated results.
Note : notice "executePendingBindings()"

when one card is deleted from RecyclerView one more card disappears along with it

I am having trouble debugging a recycler view issue. Here I am trying to delete an item from recycler view using the following method:
when I click on the card (one that is to be deleted), it will store the title of a card in shared preference. (so that in future when I open the app I will know what cards are not to be displayed)
Then I am calling a method which checks if the title in shared preference matches the title of the card and make ArrayList consisting of cards which are not present in shared preference.
Now I am using this ArrayList to fill cards using notifyDataSetChanged
My code is as Follows.
Adapter.
class LocalAdapter(var localCardsInfo : ArrayList<LocalModel>?,var fragment: LocalListingFragment) : RecyclerView.Adapter<LocalHolder>() {
fun refreshDataOnOrientationChange(mLocalCards : ArrayList<LocalModel>?){
if (localCardsInfo!!.size>0)
localCardsInfo!!.clear()
localCardsInfo = mLocalCards
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return localCardsInfo!!.size
}
override fun onBindViewHolder(holder: LocalHolder, position: Int) {
if (localCardsInfo!=null)
holder.updateUI(holder.adapterPosition,localCardsInfo!![holder.adapterPosition])
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocalHolder {
val card_a : View = LayoutInflater.from(parent.context).inflate(R.layout.card_local_a,parent,false)
return LocalHolder(card_a,fragment)
}
}
ViewHolder
class LocalHolder(itemView : View,val fragment: LocalListingFragment) : RecyclerView.ViewHolder(itemView),OpenedLayoutManagerLocal{
fun updateUI(position : Int , localModel: LocalModel){
moveButton.setOnClickListener({
archived()
})
private fun archived(){
ALL_STATIC_CONSTANTS_AND_METHODS.addToArchive(fragment.activity!!,model!!.roomName,model!!.deviceName)
itemView.archiveLayout.visibility = View.GONE
itemView.elevateLayoutLocal.visibility = View.GONE
fragment.mAdapter!!.refreshDataOnOrientationChange(ArrayList(LocalDataService.ourInstance.getNonArchivedItems(fragment.activity!!)))
}
For example, if I have six item in recycler view and when I delete one I can see 5 items on the list while debugging, even onbind is called 5 times but only 4 items are displayed.
I tried my best to debug it yet failed to find a solution and I can't think about what to do next. I tried notifyItemRemoved and notifyItemRangeInserted also but still, I am facing the same issue. It would be a great help if someone can suggest a possible cause of such issue.
Thank you.
Edits:
I just experimented by adding dummy button in fragment where recycler view is placed. An to my surprise if I delete element here just element at that position gets deleted and there is no case of disappearing items. So it seems like problem happens when I delete an item from view holder. So I guess I got the cause of the error but can't think of a way to implement it because the original button is in the card so I am forced to use delete procedure inside view holder. Please do suggest if you have any suggestions. Code that I added in fragment is as follow:
v!!.findViewById<Button>(R.id.delete_button).setOnClickListener({
try {
val model = LocalDataService.ourInstance.getNonArchivedItems(activity!!)[0]
ALL_STATIC_CONSTANTS_AND_METHODS.addToArchive(activity!!,model.roomName,model.deviceName)
mAdapter!!.refreshDataOnOrientationChange(ArrayList(LocalDataService.ourInstance.getNonArchivedItems(activity!!)))
}catch (e : Throwable){}
})
Here on this new dummy button click, it deletes the first item in the list gets deleted while on clicking the button on RecyclerView card it deletes one item and then one more item gets disappeared.

How to find out item is created in recycler view for the first time?

I have a recycler view and I want to find out if an item is created in recycler view for the first time or not?
Is there an event handler for it?
Note: I know how to implement it with flag, but I'm looking for another approach.
Why do you want this? To my mind it seems like there is not a good reason to know this information, a ViewHolder should be independent from that knowledge.
Nothing is provided for this by RecyclerView.Adapter , the only callbacks are to manage the ViewHolder instances, which can be recycled and so any particular instance wouldn't know if it is the first instance or not.
You could store a flag in your data model and access it when you set up the ViewHolder in onBindViewHolder, as you seem to know.
You can ovveride onViewAttachedToWindow(holder: ViewHolder) in your recycler adapter. sample code like this:
private var isFirstChildAttached = false // single fire
override fun onViewAttachedToWindow(holder: ViewHolder) {
super.onViewAttachedToWindow(holder)
holder.pageView.registerTopLevelTouchListener()
if (!isFirstChildAttached) {
isFirstChildAttached = true
checkAndFirePageDimen(holder.itemView)
}
}
private fun checkAndFirePageDimen(itemView: View){
itemView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
itemView.viewTreeObserver.removeOnGlobalLayoutListener(this)
pagerCallback.onFirstItemAttachedToPager(itemView)
}
})
}
"checkAndFirePageDimen" helps you for reliable itemView size.

Categories

Resources