27

I have a RecyclerView and want to allow my users to use a swipe gesture to remove items from the list. But as known from other apps (e.g. Gmail), I want to show a delete icon behind it, so that my users know that swiping results in a remove. However, I can't find an obvious way to do that. The ItemTouchHelper uses the viewHolder.itemView, so it takes the whole row.

My code:

    ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new
            ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
                @Override
                public boolean onMove(
                        final RecyclerView recyclerView,
                        final RecyclerView.ViewHolder viewHolder,
                        final RecyclerView.ViewHolder target) {
                    return false;
                }

                @Override
                public void onSwiped(
                        final RecyclerView.ViewHolder viewHolder,
                        final int swipeDir) {
                    adapter.remove(viewHolder.getAdapterPosition());
                }
            };

    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(
            simpleItemTouchCallback
    );
    itemTouchHelper.attachToRecyclerView(itemsRecyclerView);

    itemsRecyclerView.setLayoutManager(
            new LinearLayoutManager(getContext())
    );
    itemsRecyclerView.setAdapter(adapter);

Has anyone a glue if this is possible at all? The only thing I can imagine right now is to extend the ItemTouchHelper / copy the code, and instead of using viewHolder.itemView I take a view identified by an ID.

akohout
  • 1,802
  • 3
  • 23
  • 42

3 Answers3

39

I have done it with having the following layout structure for the recycler view item:

<FrameLayout
    background = dark>
    <AnyLayout with content
       android:id="@+id/removable">
    </AnyLayout>
<FrameLayout>

Then I use this view holder as base view holder in my adapter:

public class RemovableViewHolder extends RecyclerView.ViewHolder {
    private View mRemoveableView;

    public RemovableViewHolder(final View itemView) {
        super(itemView);

        mRemoveableView = itemView.findViewById(R.id.removable);
    }

    public View getSwipableView() {
        return mRemoveableView;
    }
}

In my ItemTouchHelper.Callback class I extend the following methods like that:

@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    if (viewHolder instanceof RemovableViewHolder) {
        int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
        return makeMovementFlags(0, swipeFlags);
    } else
        return 0;
}

@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    getDefaultUIUtil().clearView(((RemovableViewHolder) viewHolder).getSwipableView());
}

@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
    if (viewHolder != null) {
        getDefaultUIUtil().onSelected(((RemovableViewHolder) viewHolder).getSwipableView());
    }
}

public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    getDefaultUIUtil().onDraw(c, recyclerView, ((RemovableViewHolder) viewHolder).getSwipableView(), dX, dY,    actionState, isCurrentlyActive);
}

public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    getDefaultUIUtil().onDrawOver(c, recyclerView, ((RemovableViewHolder) viewHolder).getSwipableView(), dX, dY,    actionState, isCurrentlyActive);
}

With this approach you use one level in layouts more, but saves yourself troubles with drawing on Canvas. Also you may select other views inside the item, for example save one that was touched and return it and have children of an item also swipeable.

Attaching to the recycler view:

final ItemTouchHelper.Callback callback = new RemovableItemTouchHelperCallback(mAdapter);
final ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(recyclerView)
philips77
  • 1,294
  • 14
  • 24
  • 1
    This is a great solution, you can even retrieve the dismissed layout, the only thing is that if I try and click on a different layout from removable, it repaints removable again :( – desgraci Sep 05 '15 at 14:13
  • Yes, in my case the whole view is removable. The background is just beneath it, not visible. But if you want to make only part of your view be swipable, I would add onTouchListeners to some views and save there the view that was touched. The getSwipableView() should return that view. Luckily the onTouch is called before any of those methods above, so you may check what view was touched in getMovementFlags(..). There are some tricks with returning true or false from onTouch if you have nested swipable views. – philips77 Sep 07 '15 at 11:22
  • @philips77 can you please specify, or better yet present a sample code. I was able to get the onClick to fire, but I had to .notifyItemChanged on the "onSwiped" which caused some flickering and inconsistency. – Joaquim Ley Nov 17 '15 at 14:49
  • @philips77 I am using onMove() for drag and drop on CardViews in a RecyclerView list. I have xml set up with a red background (for swipe to delete) and foreground. When I drag a CardView the foreground moves as expected but the background does not. Any insights how to fix so that the background moves with foreground? – AJW Aug 29 '18 at 19:59
  • @philips77 are you able to make the ItemTouchHelper class re-usable so you don't have to create a new one for each activity/recyclerview that you have in your application? – Nick Nelson Dec 06 '18 at 18:04
  • @AJW sorry, I have no idea. I don't allow to move rows. – philips77 Dec 11 '18 at 09:13
  • @NickNelson, I've checked the code and I'm actually creating a new instance each time. – philips77 Dec 11 '18 at 09:14
  • @philips77, I mean in this code here where we are hard-coding the casting of the Class: ((RemovableViewHolder) viewHolder). That means we can only use this ItemTouchHelper for the "RemoveableViewHolder" class doesn't it? I'd like to use the same ItemTouchHelper class for other classes too. – Nick Nelson Dec 11 '18 at 18:44
19

use the onChildDraw() method from ItemTouchHelper - I have it working with a bitmap and coloured background for swiping left and right with different colors and icons:

        @Override
        public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {

            if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {

                View itemView = viewHolder.itemView;

                Paint paint = new Paint();
                Bitmap bitmap;

                if (dX > 0) { // swiping right
                    paint.setColor(getResources().getColor(R.color.child_view_complete));
                    bitmap = BitmapFactory.decodeResource(getApplicationContext().getResources(), R.mipmap.ic_circle_complete);
                    float height = (itemView.getHeight() / 2) - (bitmap.getHeight() / 2);

                    c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX, (float) itemView.getBottom(), paint);
                    c.drawBitmap(bitmap, 96f, (float) itemView.getTop() + height, null);

                } else { // swiping left
                    paint.setColor(getResources().getColor(R.color.primaryColorAccent));
                    bitmap = BitmapFactory.decodeResource(getApplicationContext().getResources(), R.mipmap.ic_circle_bin);
                    float height = (itemView.getHeight() / 2) - (bitmap.getHeight() / 2);
                    float bitmapWidth = bitmap.getWidth();

                    c.drawRect((float) itemView.getRight() + dX, (float) itemView.getTop(), (float) itemView.getRight(), (float) itemView.getBottom(), paint);
                    c.drawBitmap(bitmap, ((float) itemView.getRight() - bitmapWidth) - 96f, (float) itemView.getTop() + height, null);
                }

                super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);


            }
        }

96f can be replaced with whatever distance from the left/right side you'd like it to be. This isn't perfect as when I'm down to 2 items in the adapter and I remove a position the canvas does not disappear and remains until the adapter is set again - working on trying to resolve now - I'll update if I find a complete solution but for now this is what I think you're looking for.

Mark
  • 9,604
  • 5
  • 36
  • 64
  • So you have extended ItemTouchHelper and overriden onChildDraw? – akohout Jun 29 '15 at 18:21
  • Yes, its an available method in ItemTouchHelper – Mark Jun 29 '15 at 19:50
  • 6
    You should not call new inside `onChildDraw`. Also you must call `bitmap.recycle()`. This is bad memory management. – sagus_helgy Aug 06 '15 at 08:20
  • On 2 bitmaps totaling 3k? will GC not automatically handle the recycling when it decides? I can see your point if I'm dealing with lots of bitmaps flying around, however how would calling bitmap.recycle() in this instance be needed, or not calling it be bad memory management? I just want to learn from your comment so I can understand what you mean as you haven't explained, and you said it must be called. – Mark Aug 06 '15 at 10:52
  • 2
    onDraw method is called very often so 3k is gonna explode pretty soon. GC will kick in but it will take some time and that will make your animations stutter and drop frames. From the docs: "Creating objects ahead of time is an important optimization. Views are redrawn very frequently, and many drawing objects require expensive initialization. Creating drawing objects within your onDraw() method significantly reduces performance and can make your UI appear sluggish." http://developer.android.com/training/custom-views/custom-drawing.html – Nemanja Kovacevic Jan 08 '16 at 23:31
  • 1
    As for your issue when you're down to 2 items: canvas shouldn't disappear, it's the canvas for the recycler view. You are drawing on it when you shouldn't is what I think is happening. Looks like ItemTouchHelper keeps ViewHolders of removed rows in case they need to be restored. It's also calling onChildDraw for those VHs in addition to the VH being swiped. You don't want to draw for VHs that were removed earlier. You can check if viewHolder.getAdapterPosition() == -1 and not draw for those instances. – Nemanja Kovacevic Jan 08 '16 at 23:36
0

Kotlin and OnMove invisible

    val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder)
    : Boolean = false

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        val pos = viewHolder.absoluteAdapterPosition
        Toast.makeText(viewHolder.itemView.context, "Deleted $pos", Toast.LENGTH_SHORT).show()
        TODO("REMOVE ITEM")
        viewHolder.bindingAdapter?.notifyItemRemoved(pos)
    }

    override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
        getDefaultUIUtil().clearView((viewHolder as CustomViewHolder).layout)
    }
    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        if(actionState == ItemTouchHelper.ACTION_STATE_DRAG){
            (viewHolder as? CustomViewHolder)?.backgroundView?.visibility = View.GONE
        }
        viewHolder?.let { getDefaultUIUtil().onSelected((viewHolder as CustomViewHolder).layout) }
    }
    override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) = getDefaultUIUtil().onDraw(c, recyclerView, (viewHolder as CustomViewHolder).layout, dX, dY, actionState, isCurrentlyActive)
    override fun onChildDrawOver(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder?, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) = getDefaultUIUtil().onDrawOver(c, recyclerView, (viewHolder as CustomViewHolder).layout, dX, dY, actionState, isCurrentlyActive)
}

View

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/background"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:backgroundTint="@color/red">

    <!-- YOUR Background -->

    </FrameLayout>

<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<!-- YOUR View -->

</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

Add to your recyclerView

with(yourRecyclerView){
    layoutManager = LinearLayoutManager(header.context)
    adapter = ItemRvAdapter(onClick, PlaceholderContent.ITEMS)
    ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(this)
}
Lex
  • 1
  • 2