I have RecyclerView Drag & Drop feature, but I'd like to do some calculations onDrop. When I put my expensiveFunction() in onMove() it's triggered at every position change until the drag is over. That's a big overkill. Is there a way to trigger function on drag end?
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
private var simpleCallback = object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP.or(ItemTouchHelper.DOWN), 0) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val startPosition = viewHolder.absoluteAdapterPosition
val endPosition = target.absoluteAdapterPosition
Collections.swap(itemList, startPosition, endPosition)
recyclerView.adapter?.notifyItemMoved(startPosition, endPosition)
expensiveFunction()
return true
}
}
You could override onSelectedChanged() which get called when the ViewHolder swiped or dragged by the ItemTouchHelper.
To catch the drop action examine the actionState value to be ItemTouchHelper.ACTION_STATE_IDLE:
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
when (actionState) {
// when the item is dropped
ItemTouchHelper.ACTION_STATE_IDLE -> {
Log.d(TAG, "Item is dropped")
}
}
}
Related
RecyclerView drag & drop works perfectly by attaching ItemTouchHelper to RecyclerView. Like below:
abstract class RecyclerViewDragDetector : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder?.itemView?.alpha = 0.5f
}
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = 1.0f
}
}
I am able to drag & drop items using above code, but the problem is while dragging an item, that position remains blank just like empty. My requirement is to keep a background place holder visible while dragging an item.
I want something like below image.
Any suggestion is really really appreciated.
I use RecyclerView's ItemTouchHelper to change the order(drag & drop),And I want to move only a part of the list.
I don't want to move after a certain index. How can I handle it with one RecyclerView?
If I explain a little more about what I want to make,
The active items are sorted up and the non-active items are sorted down. And only the activated items will shift the order.
Right now, when I drag an active item, it goes down to the area of the non-active item, but I want to prevent it from going down. How can I do it?
UPDATE
open class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) :
ItemTouchHelper.Callback() {
private var isLongPressDrag = true
fun setLongPressDragEnable(isLongPressDrag: Boolean) {
this.isLongPressDrag = isLongPressDrag
}
override fun isLongPressDragEnabled() = isLongPressDrag
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val swipeFlags = 0
return makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return mAdapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, i: Int) {
mAdapter.onItemDismiss(viewHolder.adapterPosition)
}
}
Use ItemTouchHelper.SimpleCallback and override getDragDirs and onMove methods:
ItemTouchHelper.SimpleCallback(
UP or DOWN,
0
) {
override fun getDragDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val selectedPosition = viewHolder.adapterPosition
val isActive: Boolean = // retrieve your model from list and check its active state
return if (isActive) super.getDragDirs(recyclerView, viewHolder) else 0
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
): Boolean {
val toPosition = target.adapterPosition
val isActiveTarget: Boolean = // retrieve your target model from list and check its active state
if (!isActiveTarget) return false
val fromPosition = viewHolder.adapterPosition
moveItem(fromPosition, toPosition)
return true
}
}
Returning 0 on getDragDirs prevents inactive item selection, while returning false on onMove prevents item movement while dragging.
I have 2-way swipe options, left swipe for delete and right swipe for other action.
I want the right swipe won't dismiss the item, after the swiping the item should go back to where he was.
How can I implement this?
Here is my ItemTouchHelper class (I'm not sure if you'll need more code, if you are please leave a comment and I'll add the relevant code)
ItemTouchHelper:
class ArticleItemTouchHelper(
context: Context,
private val adapter: ArticleListAdapter
) : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val flags = ItemTouchHelper.START or ItemTouchHelper.END
return makeMovementFlags(0, flags)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false // We don't want support moving items up/down
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (direction == ItemTouchHelper.START) {
adapter.onSwipedLeft(viewHolder.adapterPosition)
} else if (direction == ItemTouchHelper.END) {
adapter.onSwipedRight(viewHolder.adapterPosition)
}
}
override fun isLongPressDragEnabled(): Boolean {
return false
}
override fun isItemViewSwipeEnabled(): Boolean {
return !adapter.isMultiSelectionActive()
}
}
I added adapter.notifyItemChanged(position) to the end of the onSwiped() method to achieve the goal.
As the data set is not changed by myself, so the item is still present for the adapter the whole item will move back to the initial position.
In your case, it would be like this:
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (direction == ItemTouchHelper.START) {
adapter.onSwipedLeft(viewHolder.adapterPosition)
} else if (direction == ItemTouchHelper.END) {
adapter.onSwipedRight(viewHolder.adapterPosition)
}
adapter.notifyItemChanged(viewHolder.adapterPosition)
}
The issue I'm having is that when dragging an item on my RecyclerView, the onMove() callback can not change the actual order of the array of items because that array is managed by PagedListAdapter. If I do it on the DB, this happens on a background thread because Room requires me to do that (and I really should do DB on the background thread anyway), and as you can imagine, that doesn't work well with the dragging of the item.
My understanding of how ItemTouchHelper works is that on onMove() I have to rearrange whatever I want to be shown visually and then I can save to the DB on clearView(). But I can't rearrange on onMove().
This is a simplification of my current code, which works but the visual cues of the drag don't work well.
inner class SwipeCallback() : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val item1 = getItem(viewHolder.adapterPosition)
val item2 = getItem(target.adapterPosition)
if (item1 != null && item2 != null) {
//BASICALLY JUST SWAPPING THE TWO POSITIONS ON THE OBJECTS but not doing anything with the adapter array because the PagedListAdapter doesn't allow me to modify the array as far I know.
swapItems(item1,item2)
}
return true
}
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun isLongPressDragEnabled(): Boolean {
return false
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ACTION_STATE_DRAG) {
viewHolder?.itemView?.alpha = 0.5f
}
}
override fun clearView(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder?.itemView?.alpha = 1.0f
//THIS IS WHERE I UPDATED THE DB, currentList is the PagedListAdapter array. This is an inner class of my PagedListAdapter.
currentList?.let {updateList(it) }
}
}
So how can I make this work visually?
Thanks.
Update: I created an example here
I assume you are using Room Database to load paging data to UI. In fact, if using Pagination library, that's the only way to make our list be able to response to dynamically change in data.
In adapter, you just need to notify database to change in onMove() function
class SwipeCallback(
private val adapter: HomeSpotAdapter,
private val onItemMove: (from: Int, to: Int) -> Unit
) : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.adapterPosition
val to = target.adapterPosition
val item1 = adapter.getItem(from)
val item2 = adapter.getItem(to)
if (item1 != null && item2 != null) {
// Notify database to swap items here
onItemMove(from, to)
return true
}
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun isLongPressDragEnabled(): Boolean {
// It has to be true
return true
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ACTION_STATE_DRAG) {
viewHolder?.itemView?.alpha = 0.5f
}
}
override fun clearView(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = 1.0f
// Don't need to do anything here
}
}
In fragment, we pass onItemMove lambda function to adapter
ItemTouchHelper(HomeSpotAdapter.SwipeCallback(adapter) { from, to ->
viewModel.swapItems(from, to)
}).apply {
attachToRecyclerView(recyclerView.getRecycler())
}
To swap items in db, basically we can use a swapItems() function like this
fun swapItem(from: Int, to: Int) {
// I'm using executor to run query in worker thread here, but you can choose whatever you want
Executors.newSingleThreadExecutor().execute {
val item1 = db.HomeSpotDao().getSpot(from)
val item2 = db.HomeSpotDao().getSpot(to)
// Swap index of 2 items
val newItem1 = item1.copy()
newItem1.indexInResponse = item2.indexInResponse
val newItem2 = item2.copy()
newItem2.indexInResponse = item1.indexInResponse
db.HomeSpotDao().updateSpot(newItem1)
db.HomeSpotDao().updateSpot(newItem2)
}
}
You can take a look at this class in Google Sample to understand how indexInResponse is used to keep tracking index of items in list
Link google sample here
Finally found the right solution.
On onMove you have to notifyItemMoved(from,to) and then on clearView you actually make the DB move. Depending on what your move involves, you will probably have to store the first from on notifyItemMoved and then calculate where it is being moved to and modify all the times in between on your DB.
I've created drag and drop for my recycler view, but I want disable drag and drop option on pull of cells (it's a headers in my view). How to make them not available for drag and drop function?
Drag And drop helper
class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
override fun isLongPressDragEnabled() = true
override fun isItemViewSwipeEnabled() = true
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
mAdapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
return true
}
}
Adapter for Recyclerview
interface ItemTouchHelperAdapter {
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
}
Code to pair adapter to rv
val drugAndDropHandler = SimpleItemTouchHelperCallback(adapter)
ItemTouchHelper(drugAndDropHandler).attachToRecyclerView(multiple_stores_list)
Just need to override getMovementFlags
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
if (viewHolder.adapterPosition in 0..NOT_DRAGABLE_ITEMS_MAX_POSITION) {
return 0
}
}