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.
Related
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")
}
}
}
1.itemTouchHelper works well at first.
Kotlin code as followed
val itemTouchHelper = ItemTouchHelper(object: ItemTouchHelper.Callback(){
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder:RecyclerView.ViewHolder): Int{
val dragFlag = ItemTouchHelper.UP | ItemTouchHelper.Down
val swipeFlags = 0
return makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder){
val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition
Collections.swap(mList, fromPosition, toPosition)
mAdapter.notifyItemMoved(fromPosition, toPosition)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int){
}
})
itemTouchHelper.attachToRecyclerView(mRecyclerView)
2.if I setPadding to RecyclerView, there is something wrong when I try to drag item
mRecyclerView.setPadding(0,250,0,0) // (left, top, right ,bottom)
we can drag items easily when recyclerview is at its top(recyclerview has not been scrolled, it is at its top). However, if the recyclerview is not at its top(recyclerview has been scrolled downwards), when I drag the item it will go to the toppest position immediately instead of the position I drag to.
watch the gif for more details, somebody help me :(
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)
}
So I've created an interface extending ItemToucHelper.Callback() and when I move my viewholder it changes the color of the stroke and when I drop it, it should return to the normal color.
The problem is that after dropping it doesn't change the color even though the Log shows it calls the clearview methodm then after "touching" the item sometimes it returns to normal.
Relevant code
Custom class for Callback
class NoteItemTouchHelperCallback(
private val itemTouchHelper: ItemTouchHelperAdapter
): ItemTouchHelper.Callback() {
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
Log.i(TAG, "Clear View")
viewHolder.itemView.itemCardView.apply {
changeStrokeColor(R.color.strokeCard)
changeStrokeWidth(2)
}
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if(actionState == ItemTouchHelper.ACTION_STATE_DRAG){
viewHolder?.itemView?.itemCardView?.apply {
Log.i(TAG, "On Selected")
//I commented this and it's not the problem, but I know onSelectedChanged can be helpful here to solve it
//changeStrokeColor(R.color.strokeCardSelected)
//changeStrokeWidth(5)
}
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
itemTouchHelper.onItemMove(viewHolder.adapterPosition, target.adapterPosition, viewHolder)
return true
}
}
Interface that will be implemented in MainActivity
interface ItemTouchHelperAdapter {
fun onItemMove(fromPosition: Int, toPosition: Int, viewHolder: RecyclerView.ViewHolder)
fun onItemSwiped(position: Int)
}
MainActivity
class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), ItemTouchHelperAdapter {
override fun onItemMove(fromPosition: Int, toPosition: Int, viewHolder: RecyclerView.ViewHolder) {
val noteList = mAdapter?.getNoteList()
val note = noteList?.get(fromPosition)
if(note!=null){
noteList.removeAt(fromPosition)
noteList.add(toPosition, note)
Log.i(TAG, "Change position")
viewHolder.itemView.itemCardView.apply {
changeStrokeColor(R.color.strokeCard)
changeStrokeWidth(5)
Log.i(TAG, "Change UI card")
}
}
mAdapter?.notifyItemMoved(fromPosition, toPosition)
}
}
I tried to put the relevants thing that matter when it comes to the action of moving the item.
Question: What am I missing in the code?
Edit: The clearview takes effect after locking and unlocking the phone. Why does this happen?
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.