I am facing an issue with ItemTouchHelper in combination with SpannedGridLayoutManager in my RecyclerView, drag is ended prematurely when dragging to the item next to the dragged one. I know it is glitchy layout manager, because it works with other layout managers without any issues.
Did somebody worked this out already?
The onSelectedChanged(RecyclerView.ViewHolder, int) callback provides information about the current actionState:
- ACTION_STATE_IDLE:
- ACTION_STATE_DRAG
- ACTION_STATE_SWIPE
So you could keep track whether the order changed, and when the state changes to ACTION_STATE_IDLE, you can do what you need to do!
Implement a callback class like this.
class CardsTouchHelperCallback extends ItemTouchHelper.Callback {
...
#Override
public boolean onMove(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getAdapterPosition();
int toPosition = target.getAdapterPosition();
dragFrom = fromPosition;
dragTo = toPosition;
mOrderChanged = true;
return false;
}
#Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
if (actionState == ItemTouchHelper.ACTION_STATE_IDLE && mOrderChanged) {
//doSomething();
touchHelperAdapter.onItemMove(dragFrom, dragTo);
mOrderChanged = false;
}
}
}
Related
This is my implementation of ItemTouchHelper.callback
I implemented getMovementFlags so that it could drag and drop in UP, Down, left, and right directions but didn't execute the onMove method
I know where the problem is because I'm in the setOnLongClickListener notifyDataSetChanged on the item and that causes it to fail to drag but I do need to go through setOnLongClickListener, okay NotifyDataSetChanged updates the item to editable style
#Override
public int getMovementFlags(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
}
#Override
public boolean onMove(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder, #NonNull RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getAdapterPosition();
int toPosition = target.getAdapterPosition();
dragAdapter.itemMove(fromPosition, toPosition);
return true;
}
LEFT and RIGHT should be used for swipe, so keep UP and DOWN only for move position.
And, onMove will be called many many times, so you should implement clearView and do the move in clearView. onMove is just used to record positions.
private val _callback = object:ItemTouchHelper.Callback() {
override fun getMovementFlags(rv: RecyclerView, h: RecyclerView.ViewHolder): Int {
val drag = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val pos = h.layoutPosition
val swipe = if (pos == _a._current) 0 else ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT // don't allow swipe current row
return makeMovementFlags(drag, swipe)
}
// onMove will be called many many times if dragging toward top/bottom
// to avoid this, make real swap at clearView which will be called only once
private var _moveFrom = -1
private var _moveTo = -1
override fun onMove(rv: RecyclerView, h: RecyclerView.ViewHolder, dst: RecyclerView.ViewHolder): Boolean {
if (_moveFrom == -1) {
_moveFrom = h.layoutPosition
}
_moveTo = dst.layoutPosition
return true
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
if (_moveFrom >= 0 && _moveTo >= 0 && _moveFrom != _moveTo) {
_layers.removeAt(_moveFrom)
_layers.add(_moveTo, layer)
if (_moveFrom < _moveTo) { // drag down
...
} else { // drag up
...
}
}
_moveFrom = -1
_moveTo = -1
Handler(Looper.getMainLooper()).post { _adapter.notifyDataSetChanged() }
}
}
I have a problem with ItemTouchHelper of RecyclerView.
I am making a game. The game board is actually a RecyclerView. RecyclerView has GridLayoutManager with some span count. I want to implement drag & drop recyclerview's items. Any item can dragging over all directions (up, down, left, right).
private void initializeLayout() {
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutFrozen(true);
recyclerView.setNestedScrollingEnabled(false);
// set layout manager
GridLayoutManager layoutManager = new GridLayoutManager(getContext(), BOARD_SIZE,
LinearLayoutManager.VERTICAL, true);
recyclerView.setLayoutManager(layoutManager);
// Extend the Callback class
ItemTouchHelper.Callback itemTouchCallback = new ItemTouchHelper.Callback() {
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
Log.w(TAG, "onMove");
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// Application does not include swipe feature.
}
#Override
public void onMoved(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
int fromPos, RecyclerView.ViewHolder target, int toPos, int x, int y) {
Log.d(TAG, "onMoved");
// this is calling every time, but I need only when user dropped item, not after every onMove function.
}
#Override
public boolean isItemViewSwipeEnabled() {
return false;
}
#Override
public boolean isLongPressDragEnabled() {
return true;
}
#Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.START | ItemTouchHelper.END;
int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
}
};
ItemTouchHelper touchHelper = new ItemTouchHelper(itemTouchCallback);
touchHelper.attachToRecyclerView(recyclerView);
}
SO, why ItemTouchHelper's onMoved function works when I still dragging item on the RecyclerView ? How can I achieve this ?
While dragging and dropping an item, the onMove() can be called more than once, but the clearView() will be called once. So you can use this to indicate the drag was over(drop was happened).
And use two variables dragFrom and dragTo to trace the really position in a completed "drag & drop".
private ItemTouchHelper.Callback dragCallback = new ItemTouchHelper.Callback() {
int dragFrom = -1;
int dragTo = -1;
#Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.UP|ItemTouchHelper.DOWN|ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT,
0);
}
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getAdapterPosition();
int toPosition = target.getAdapterPosition();
if(dragFrom == -1) {
dragFrom = fromPosition;
}
dragTo = toPosition;
adapter.onItemMove(fromPosition, toPosition);
return true;
}
private void reallyMoved(int from, int to) {
// I guessed this was what you want...
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}
#Override
public boolean isLongPressDragEnabled() {
return true;
}
#Override
public boolean isItemViewSwipeEnabled() {
return false;
}
#Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if(dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) {
reallyMoved(dragFrom, dragTo);
}
dragFrom = dragTo = -1;
}
};
adapter.onItemMove(fromPosition, toPosition) was like below:
public void onItemMove(int fromPosition, int toPosition) {
list.add(toPosition, list.remove(fromPosition));
notifyItemMoved(fromPosition, toPosition);
}
The onSelectedChanged(RecyclerView.ViewHolder, int) callback provides information about the current actionState:
- ACTION_STATE_IDLE:
- ACTION_STATE_DRAG
- ACTION_STATE_SWIPE
So you could keep track whether the order changed, and when the state changes to ACTION_STATE_IDLE, you can do what you need to do!
Example:
private final class MyCallback extends ItemTouchHelper.Callback {
private boolean mOrderChanged;
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
// Check if positions of viewHolders correspond to underlying model, and if not, flip the items in the model and set the mOrderChanged flag
mOrderChanged = true;
}
#Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
if (actionState == ItemTouchHelper.ACTION_STATE_IDLE && mOrderChanged) {
doSomething();
mOrderChanged = false;
}
}
I did some tests and onSelectedChanged(RecyclerView.ViewHolder?, Int) seemed most reliable for me to detect end of the gesture (drop). The method is called whenever an item is being dragged and passed action state of ACTION_STATE_DRAG. When the drag is over, it is called with action state of ACTION_STATE_IDLE.
See my solution below. The onItemDrag(Int, Int) callback is used for reordering items in an adapter as the item is being dragged. On the other hand the onItemDragged(Int, Int) callback is intended for updating positions in a database at the end of the gesture.
class ItemGestureHelper(private val listener: OnItemGestureListener) : ItemTouchHelper.Callback() {
interface OnItemGestureListener {
fun onItemDrag(fromPosition: Int, toPosition: Int): Boolean
fun onItemDragged(fromPosition: Int, toPosition: Int)
fun onItemSwiped(position: Int)
}
private var dragFromPosition = -1
private var dragToPosition = -1
// Other methods omitted...
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
// Item is being dragged, keep the current target position
dragToPosition = target.adapterPosition
return listener.onItemDrag(viewHolder.adapterPosition, target.adapterPosition)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
listener.onItemSwiped(viewHolder.adapterPosition)
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
when (actionState) {
ItemTouchHelper.ACTION_STATE_DRAG -> {
viewHolder?.also { dragFromPosition = it.adapterPosition }
}
ItemTouchHelper.ACTION_STATE_IDLE -> {
if (dragFromPosition != -1 && dragToPosition != -1 && dragFromPosition != dragToPosition) {
// Item successfully dragged
listener.onItemDragged(dragFromPosition, dragToPosition)
// Reset drag positions
dragFromPosition = -1
dragToPosition = -1
}
}
}
}
}
You must implement OnMove listener in you adapter:
Collections.swap(youCoolList, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
like this man doing
https://medium.com/#ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf#.blviq6jxp
special grid example
https://medium.com/#ipaulpro/drag-and-swipe-with-recyclerview-6a6f0c422efd#.xb74uu7ke
so i have implemented this itemtouchhelper.simple callback on a recyclerview rv.
now in this rv i have set 2 kinds on layout as a row depending on the content type.
so as i set this touchhelper on the rv it is being implemented on both of these layouts even though i did'nt want to do that.i only want to apply that swipe to only one type of this layout.
ItemTouchHelper.SimpleCallback ith = new ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.RIGHT) {
#Override
public boolean onMove(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder, #NonNull RecyclerView.ViewHolder viewHolder1) {
return false;
}
#Override
public void onSwiped(#NonNull RecyclerView.ViewHolder viewHolder, int i) {
if(viewHolder.itemView.findViewById(R.id.messageholder_from) != null)
{
Log.d("texts", "onSwiped: "+viewHolder.itemView.findViewById(R.id.messageholder_from).findViewById(R.id.crfrom));
}
adapter.notifyDataSetChanged();
}
};
new ItemTouchHelper(ith).attachToRecyclerView(rv);
as you can see this code i only want to implement the swipe on the row which has this messageholder_from child in it otherwise i don't want to implement the swipe.
is there any way to remove the swipe animation and the listener on this other child messageholder_to.
my app shows either a to_layout or a from_layout depending on he message id.
thanks for any kind of help.
Inside your ItemTouchHelper.SimpleCallback, override the getSwipeDirs() method and return 0 for any row that you want to disable swiping on.
#Override
public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder.itemView.findViewById(R.id.messageholder_from) == null) {
return 0;
}
return super.getSwipeDirs(recyclerView, viewHolder);
}
Depending on exactly how your app is set up, there might be a better way to detect that viewHolder is the kind you want to disallow swiping on. For example, maybe you could have
if (viewHolder instanceof WrongKindOfViewHolder)
or
if (viewHolder.isNotSwipeable)
int makeMovementFlags (int dragFlags,int swipeFlags) is responsible for each row move, swipe or drag.
From documentation :
Convenience method to create movement flags.
For instance, if you want to let your items be drag & dropped
vertically and swiped left to be dismissed, you can call this method
with: makeMovementFlags(UP | DOWN, LEFT);
Implement getMovementFlags() method and return with makeMovementFlags(0, 0); for which row or viewholder you don't want to add swipe or drag.
Example Code:
#Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = 0;
int swipeFlags = 0;
if (recyclerView.getLayoutManager().getClass() == LinearLayoutManager.class ) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int orientation = linearLayoutManager.getOrientation();
if (orientation == LinearLayoutManager.VERTICAL) {
swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
}else {
// horizontal
dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
// no swipe for horizontal layout
swipeFlags = 0;
}
}
if (viewHolder.getAdapterPosition() == 3) {
// disable swipe feature for position 3
return makeMovementFlags(0, 0);
}
return makeMovementFlags(dragFlags, swipeFlags);
}
I am trying to implement ItemTouchHelper for Horizontal Recyclerview. (setting layout manager to LinearLayoutManager with orientation LinearLayoutManager.HORIZONTAL). Example, I want to delete an item when swiped down and drag to left or right for swapping items.
All the samples I have gone through explains ItemTouchHelper for Vertical Recyclerview or items in grid.
Followed samples from following links:
https://medium.com/#ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf
https://medium.com/#xabaras/recyclerview-swiping-with-style-151e21b1af07
How can I achieve swipe down to delete and drag sideways to swap items in Horizontal Recyclerview?
You can use this simple code to achieve the swipe down to remove.
ItemTouchHelper.SimpleCallback simpleCallback = new ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.DOWN) {
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
final int position = viewHolder.getLayoutPosition();
if (direction == ItemTouchHelper.DOWN) {
//your code for deleting the item from database or from the list
adapter.removeNote(position);
adapter.notifyItemRemoved(position)
}
}
};
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleCallback);
itemTouchHelper.attachToRecyclerView(recyclerView);
For Kotlin geeks below is the code for the same -
val simpleCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.DOWN) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (direction == ItemTouchHelper.DOWN) {
//your code for deleting the item from database or from the list
val position = viewHolder.adapterPosition
noteList.removeAt(position)
adapter.notifyItemRemoved(position)
}
}
}
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recycler_view)
You only need to change the method "getMovementFlags".
#Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
to
#Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
final int swipeFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
final int dragFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
To delete an item, create a new method in "ItemTouchHelperAdapter" called "onSwiped" and implement it to remove the item
#Override
public boolean onSwiped(int itemPosition, int direction) {
if(direction == SimpleItemTouchHelperCallback.SWIPED_TO_END) {
list.remove(itemPosition);
notifyItemRemoved(itemPosition);
}else{
notifyItemChanged(itemPosition);
}
return true;
}
i have a problem figuring out how to give cardViews temporarily elevation on drag. I use a recycler view with cardViews and this is my itemtouchhelper:
class ListTouchHelper extends ItemTouchHelper.Callback {
private final ActionCompletionContract contract;
public ListTouchHelper(ActionCompletionContract contract) {
this.contract = contract;
}
#Override
public int getMovementFlags(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
return makeMovementFlags(dragFlags, swipeFlags);
}
#Override
public boolean onMove(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder, #NonNull RecyclerView.ViewHolder target) {
contract.onViewMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
#Override
public void onSwiped(#NonNull RecyclerView.ViewHolder viewHolder, int direction) {
if (direction == ItemTouchHelper.LEFT) {
contract.onViewSwipedLeft(viewHolder.getAdapterPosition());
} else if (direction == ItemTouchHelper.RIGHT) {
contract.onViewSwipedRight(viewHolder.getAdapterPosition());
}
}
public interface ActionCompletionContract {
void onViewMoved(int oldPosition, int newPosition);
void onViewSwipedLeft(int position);
void onViewSwipedRight(int position);
}
}
I have managed to give it temporarily elevation with:
Which resulted in: (the shadows are somehow clipped?)
However, once the view is just slightly moved, the elevation disappears:
My question is: how do i get the elevation (including shadows) when the cards are being dragged?
Thanks in advance!
#LivinTheNoobLife in your solution you are using the ViewPropertyAnimator, but you are not setting any translation to it, hence, no animations will be applied.
This is my solution with a properly working floating animation:
class DragHelper extends ItemTouchHelper.Callback {
private boolean cardPicked = true;
private boolean reset = false;
#Override
public void onChildDraw(#NonNull Canvas c, #NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
// elevate only when picked for the first time
if (cardPicked) {
ViewPropertyAnimator animator = viewHolder.itemView.animate();
animator.translationZ(16);
animator.setDuration(200);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.start();
cardPicked = false;
}
// when your item is not floating anymore
if (reset){
ViewPropertyAnimator animator = viewHolder.itemView.animate();
animator.translationZ(0);
animator.setDuration(200);
animator.setInterpolator(new AccelerateInterpolator());
animator.start();
cardPicked = true;
reset = false;
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
// As the doc says
// Called by the ItemTouchHelper when the user interaction with an element is over and it also completed its animation.
#Override
public void clearView(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
// interaction is over, time to reset our elevation
reset = true;
}
}
Ok so I solved it, kindof. I think for some other reason its not working by default, however I managed to write a workaround.
private boolean first = true; //first draw of cardView?
private boolean last = false; //last draw of cardView?
#Override
public void onChildDraw(#NonNull Canvas c, #NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
//add elevation on first draw
if (first) {
ViewPropertyAnimator animator = viewHolder.itemView.animate();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //consider SDK version
viewHolder.itemView.setTranslationZ(7);
animator.start();
}
first = false;
}
//remove translationZ in last edit
if (last) {
ViewPropertyAnimator animator = viewHolder.itemView.animate();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //consider SDK version
viewHolder.itemView.setTranslationZ(0);
animator.start();
}
//reset values
last=false;
first=true;
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
#Override
public void clearView(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
last = true; //only one more OnChildDrawWillBeCalled
}
The above code is added to your ItemTouchHelper.Callback and all should work.
The basic idea is to manually control the drawing of the translationz.
For that I figure out when is the first canvas drawn, and when the last, whereas the cancas will show the shadow.
Maxbe one more comment: the ViewPropertyAnimator in combination with the xml layout file is incredibly unintuitive and buggy, so if you can avoided I would reccomend to do so, and instead try programmatically animating the view changes and effects.
Hope this can help someone.