47

I have been doing some research and I have yet to find an example or implementation that allows you to put a view (Example Image Below) underneath the RecyclerView when you swipe. Does anyone have any example or an idea of how this would be implemented WITHOUT using a library.

Here is how I am implementing the swipe to dismiss.

ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        return false;
    }

    @Override
    public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        return super.getSwipeDirs(recyclerView, viewHolder);
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        if (viewHolder instanceof ViewDividers) {
            Log.e("DIRECTION", direction + "");
        }
    }
};
new ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(recycler);

Here is the example of Google's Inbox: enter image description here

Eugene H
  • 3,520
  • 3
  • 22
  • 39
  • that's funny that you asked that question one day later than me: http://stackoverflow.com/questions/32204874/showing-custom-view-under-swiped-recyclerview-item . I tried drawing in `onChildDraw`, but with no luck. I'm going to watch your question and hopefully we both solve that problem, – spoko Sep 04 '15 at 12:18
  • Ha. Yea I'll figure a way Today. I have a rugged way working now with a swiping library. If I can't get it to work the other way I will send you what I have. – Eugene H Sep 04 '15 at 12:51
  • Have you had any luck with this? – spoko Sep 13 '15 at 15:27
  • 2
    @spoko Not the way I wanted to implement it. I ended up using https://github.com/daimajia/AndroidSwipeLayout to implement the swipe I wanted. But you would need to implement the swipe inside the ViewHolder – Eugene H Sep 13 '15 at 16:07
  • Thanks, that library partially solved the problem. It's definitely not perfect, swipes are not really how they should be (for example if you start swiping one direction, you can't swipe the other one), but it's a good start. – spoko Sep 14 '15 at 20:49
  • @spoko Came across that issue as well. Here is the solution to the swipe. swipeLayout.setRightSwipeEnabled(false); – Eugene H Sep 14 '15 at 23:40
  • I have made small library which use ItemTouchHelper to make gestures creation and adding background to items easier, you can find it here github.com/olmur/rvtools – Olexii Muraviov Feb 08 '17 at 11:53
  • @Olexii Muraviov Using onMove() with ItemTouchHelper, I am able to drag and drop my CardViews in a RecyclerView list. However, the CardViews background does not move with the foreground. Do you have any advice or insight on how to fix? – AJW Aug 29 '18 at 19:45

6 Answers6

29

My understanding of how this is done is that one would put two views in the xml that would be displayed per line in your recyclerview.

So for example, this would be my adapter:

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    public static class ExampleViewHolder extends RecyclerView.ViewHolder {
        public TextView background;
        public TextView foreground;

        public ExampleViewHolder(View v) {
            super(v);
            background = (TextView) v.findViewById(R.id.background);
            foreground = (TextView) v.findViewById(R.id.foreground);
        }

        @Override
        public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
            if (holder instanceof ExampleViewHolder) {
                ((ExampleViewHolder) holder).background.setBackgroundColor(); // do your manipulation of background and foreground here.
            }
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                          int viewType) {

            View v = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.example, parent, false);
            return new ExampleViewHolder(v);
        }


    }
}

Each line in the recyclerview is pulling the xml layout from R.layout.example. Therefore, to create a view underneath, you can just use relativelayout or framelayout to create the views on top of one another:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/background"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/foreground"/>
</RelativeLayout>

Then if you do not want to use a library for the swipe, you can copy this class from google and subsequently modified by Bruno Romeu Nunes:

https://github.com/heruoxin/Clip-Stack/blob/master/app/src/main/java/com/catchingnow/tinyclipboardmanager/SwipeableRecyclerViewTouchListener.java

The class will require you to create a swipe listener:

swipeTouchListener =
        new SwipeableRecyclerViewTouchListener(mRecyclerView,
                new SwipeableRecyclerViewTouchListener.SwipeListener() {
                    @Override
                    public boolean canSwipe(int position) {
                        if (position == totalPost.size() - 1 && !connected) {
                            return false;
                        }
                        return true;
                    }

                    @Override
                    public void onDismissedBySwipeLeft(RecyclerView recyclerView, int[] reverseSortedPositions) {
                        for (int position : reverseSortedPositions) {
                            //change some data if you swipe left
                        }
                        myAdapter.notifyDataSetChanged();
                    }

                    @Override
                    public void onDismissedBySwipeRight(RecyclerView recyclerView, int[] reverseSortedPositions) {
                        for (int position : reverseSortedPositions) {
                            //change some data if you swipe right
                        }
                        myAdapter.notifyDataSetChanged();
                    }
                });

Then simply link it with your recyclerview:

    mRecyclerView.addOnItemTouchListener(swipeTouchListener);
Simon
  • 19,658
  • 27
  • 149
  • 217
  • Sorry for not getting back sooner. I have been quite busy with work. I will try it in the AM and get back to you. – Eugene H Sep 02 '15 at 02:48
  • Eugene, did the Touchlistener work for you? I think if you want the top view to swipe, you will need to only add it to the top view. – Simon Sep 04 '15 at 11:30
  • I am going to be implementing it today at work. I didn't want the other person to get the answer because he clearly didn't read my question. But there is a reference to that notes app which implements what I am looking for. Mind if I get back to you if I have any issues? – Eugene H Sep 04 '15 at 11:47
  • So I used this implementation and I am not able to get it to display the background underneath the foreground view. It allows swiping and dismissing of the foreground view (although there is always still a placeholder for it), but it doesn't ever show the background view. – jwir3 Dec 01 '15 at 01:50
  • Have you tried putting your background view underneath your foreground view in a framelayout? I suspect this is an xml issue and not a swiping issue. – Simon Dec 01 '15 at 18:27
  • 10
    I'm not quite sure how this would even work. When you swipe a recycle cell item your are swiping the whole view not the the top of the frame. So you would always swipe away the background view along with the foreground. @Simon do you have an example of this working? – Andres Castro Feb 21 '16 at 20:05
  • @Andres, I do not have a working example of this - it was just an idea of how I would do it if i had the same problem. After looking at the answer again though, the SwipeableRecyclerViewTouchListener.java class was made by google for recyclerview, not necessarily for view. I would modify the class to implement View.OnTouchListener instead of RecyclerView.OnItemTouchListener and update the methods in the class for Views, not recyclerview. Then attach the touchlistener to your foreground and you should be good to go. – Simon Feb 22 '16 at 07:46
  • @Simon Can we partially swipe the item? just as shown in the image in the main Question. – Meet Vora May 09 '16 at 06:22
  • @AndresCastro The constructor of `SwipeableRecyclerViewTouchListener` takes `mBgID` and `mFgID` as params. And it does what is required to only swipe view with `mFgID` and show `mBgID` when user tries to drag an item. – M-Wajeeh Mar 08 '17 at 04:39
  • onDismissedBySwipeLeft onDismissedBySwipeRight methods does not exists in provided class – Adeel Turk Mar 14 '17 at 10:37
  • If every item in your recycler view has a foreground and a (usually not visible) background, you are going to get a **lot** of overdraw. You are basically have an entire view layer that is rarely used. See @Ali Mehrpour 's answer on how to reduce this – tir38 Apr 13 '17 at 00:03
12

I was investigating the same issue.
What i ended up doing was, in the layout that contained the recyclerView, I added a simple FrameLayout called 'swipe_bg' of the same height as one of my RecyclerView.ViewHolder items. I set its visibility to "gone", and placed it under the RecyclerView

Then in my activity where i set the ItemTouchHelper, I override the onChildDraw like so..

ItemTouchHelper.SimpleCallback swipeCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT) {

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
         View itemView = viewHolder.itemView;
         Imageview swipe_bg = itemView.findViewById(R.id.imageview_swipe_background);
         swipe_bg.setY(itemView.getTop());
         if (isCurrentlyActive) {
             swipe_bg.setVisibility(View.VISIBLE);
         } else {
             swipe_bg.setVisibility(View.GONE);
         }
         super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }
};

Don't know if that is the best way, but seemed like the simplest way.

The Fluffy T Rex
  • 430
  • 7
  • 22
erik
  • 4,946
  • 13
  • 70
  • 120
8

I like the @erik approach but I would recommend to drawing what you want via passed Canvas to onChildDraw() function. e.g. item background or icon.

@Override 
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    final ColorDrawable background = new ColorDrawable(Color.RED);
    background.setBounds(0, itemView.getTop(),   itemView.getLeft() + dX, itemView.getBottom());
    background.draw(c);

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

In this approach, you will draw what needed on demand and no need to inflate unused views

Ali Mehrpour
  • 603
  • 8
  • 16
  • What is background in this case? – Lion789 Oct 17 '16 at 03:09
  • The background type can be Drawable or ColorDrawable. – Ali Mehrpour Oct 17 '16 at 16:35
  • The color in the example will only show red if swiped left to right. For right to left you'll need something like background.setBounds((itemView.getRight()+dX).toInt(), itemView.getTop(), itemView.getRight(), itemView.getBottom()) With this and checking if the dX value is > 0 or less than 0 (which tells you the direction of movement) you'll also be able to have a different color for each swipe direction if wanted – Andrew Sep 18 '17 at 15:50
3

Simple solution without allocation or drawing on canvas. SomeAdapter.SomeVH should contain upper view and under view. And with this approach we will be able to swipe only upper view (container), exposing under view (with label, icon whatever you want)

class SomeTouchHelper extends ItemTouchHelper.Callback {
...
    @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) {
            if (viewHolder instanceof SomeAdapter.SomeVH) {
                SomeAdapter.SomeVH someViewHolder
                        = (SomeAdapter.SomeVH) viewHolder;
                ViewCompat.setTranslationX(someViewHolder.mContainer, dX);
            }
        } else {
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        }
    }
...
}

Attach it

new ItemTouchHelper(SomeTouchHelper).attachToRecyclerView(recyclerView);

don't forget to restore initial view state of SomeVH in adapter onViewRecycled()

public void onViewRecycled(final SomeVH holder) {
        if (holder.mContainer != null) {
            holder.mContainer.setTranslationX(0);//restore position
        }
}
Yazazzello
  • 6,053
  • 1
  • 19
  • 21
0

Changing elevation works for me to bring one item above another one.

mrj
  • 589
  • 1
  • 7
  • 17
-1

You can use RvTools Library. It will help you easily implement what you want.

Just create your SwipeContextMenuDrawer with desired look

public class YourSwipeContextMenuDrawer extends SwipeContextMenuDrawer {

private final Paint mRightPaint;
private final Paint mIconPaint;
private final Bitmap mRightIconBitmap;

public YourSwipeContextMenuDrawer(@NonNull Context context) {
    mRightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mRightPaint.setColor(ContextCompat.getColor(context, R.color.deep_orange_400));
    mRightIconBitmap = GraphicUtils.getBitmap(context, R.drawable.ic_delete, 100, 100);
    mIconPaint = new Paint();
    mIconPaint.setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(context, R.color.backgroundColor), PorterDuff.Mode.SRC_IN));
}

@Override
public void drawRight(@NonNull Canvas canvas, @NonNull View view) {
    canvas.drawRect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom(), mRightPaint);
    canvas.drawBitmap(mRightIconBitmap, view.getLeft() + mRightIconBitmap.getWidth() - 20, (view.getBottom() + view.getTop() - mRightIconBitmap.getHeight()) >> 1, mIconPaint);
}

@Override
public void drawLeft(@NonNull Canvas canvas, @NonNull View view) {
}
}

And create RvTools instance with swipe action and swipe context menu drawer like this

 new RvTools.Builder(recyclerView)
            .withSwipeRightAction(this)
            .withSwipeContextMenuDrawer(new YourSwipeContextMenuDrawer(getContext()))
            .buildAndApplyToRecyclerView();
Olexii Muraviov
  • 1,456
  • 1
  • 12
  • 36