15

First of all, I saw this question: Adding a colored background with text/icon under swiped row when using Android's RecyclerView

However, even though the title states "with text/icon", the answer only covers using Canvas object to draw a rectangle.

Now I have drawing a green rectangle implemented; however I want to make it more obvious what is going to happen by adding "done" button, just like on the following screenshot.

enter image description here

I created a custom view:

public class ChoosePlanView extends LinearLayout{

    public ChoosePlanView(Context context) {
        super(context);
        init();
    }

    public ChoosePlanView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ChoosePlanView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        View v = inflate(getContext(), R.layout.choose_plan_view, null);
        addView(v);
    }
}

And the layout is very simple, consisting of green background and the icon:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" 
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/choose_green">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/doneIcon"
        android:layout_gravity="center_horizontal|right"
        android:src="@mipmap/done"/>

</LinearLayout>

I tried overriding method OnChildDraw:

// ChoosePlanView choosePlanView; <- that was declared and initialized earlier, so I don't have to create a new ChoosePlanView everytime in OnChildDraw();

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

/*
    onMove, onSwiped omitted
*/
  public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
       View itemView = viewHolder.itemView;

       if(dX < 0){
            choosePlanView.invalidate();
            //choosePlanView.setBackgroundResource(R.color.delete_red);
            choosePlanView.measure(itemView.getWidth(), itemView.getHeight());
            choosePlanView.layout(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom());
            c.save();
            c.translate(choosePlanView.getRight() + (int) dX, viewHolder.getAdapterPosition()*itemView.getHeight());

            choosePlanView.draw(c);
            c.restore();
       }

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

And it kinda works (draws my ChoosePlanView), but there are two problems.

First of all it draws only a small green square wrapping my "done" icon (the red part of the view seen on the screenshot below comes from commented line choosePlanView.setBackgroundResource(R.color.delete_red);). The view has the correct width though, when checked in debugger.

Second thing is where the icon is placed. I specified that I want it to be centered horizontally and always "sticked" to the right side of the view. However it moves, when the RecyclerView item is is swiped, and having red background shows it isn't placed in the center either.

I tried adding line choosePlanView.invalidate(), because I thought that happens, because the view was created earlier, but it seems redrawing changes nothing.

enter image description here

Community
  • 1
  • 1
spoko
  • 783
  • 1
  • 10
  • 24
  • In your onChildDraw method you are drawing background for checkmark image? If so, Image has wrap_content/wrap_content size... therefor background is only the little square – VizGhar Aug 25 '15 at 13:15
  • for positioning use Relative layout and for child: android:layout_centerVertical="true" android:layout_alignParentRight="true" – VizGhar Aug 25 '15 at 13:21
  • @VizGhar In my onChildDraw I used the line `choosePlanView.setBackgroundResource(R.color.delete_red);` only for debugging purpose, to show wrong checkmark image postition in the layout. When I comment this line, this image still has wrap_content/wrap_content size and there is no background at all. Changing LinearLayout to RelativeLayout didn't change image position. – spoko Aug 25 '15 at 13:29
  • "I tried overriding method OnChildDraw" What is parent class? I'm just not sure why are you displaying ChoosePlanView programmatically – VizGhar Aug 25 '15 at 13:42
  • @VizGhar ahh, sorry, I already edited it. Parent class is `ItemTouchHelper.SimpleCallback` used to implement swiping. – spoko Aug 25 '15 at 13:46
  • You could also use `Drawable.draw(canvas)` to draw the icon directly instead of manually laying out views. – Qw4z1 Aug 28 '15 at 11:59
  • I would also investigate these lines View v = inflate(getContext(), R.layout.choose_plan_view, null); addView(v); What you do here is placing the green linear layout inside the red linear layout, but what LayoutParams does the red one have that could affect the green one? – Qw4z1 Aug 28 '15 at 12:17

5 Answers5

21

Using the ItemTouchUiUtil interface provides a robust solution to this. It allows you to have a foreground and background view in your ViewHolder and delegate all the swipe handling to the foreground view. Here is an example that I use for swipe right to remove.

In your ItemTouchHelper.Callback :

public class ClipItemTouchHelper extends ItemTouchHelper.SimpleCallback {

    ...

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        if (viewHolder != null){
            final View foregroundView = ((ClipViewHolder) viewHolder).clipForeground;

            getDefaultUIUtil().onSelected(foregroundView);
        }
    }

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView,
                            RecyclerView.ViewHolder viewHolder, float dX, float dY,
                            int actionState, boolean isCurrentlyActive) {
        final View foregroundView = ((ClipViewHolder) viewHolder).clipForeground;

        drawBackground(viewHolder, dX, actionState);

        getDefaultUIUtil().onDraw(c, recyclerView, foregroundView, dX, dY,
                actionState, isCurrentlyActive);
     }

    @Override
    public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
                                RecyclerView.ViewHolder viewHolder, float dX, float dY,
                                int actionState, boolean isCurrentlyActive) {
        final View foregroundView = ((ClipViewHolder) viewHolder).clipForeground;

        drawBackground(viewHolder, dX, actionState);

        getDefaultUIUtil().onDrawOver(c, recyclerView, foregroundView, dX, dY,
                actionState, isCurrentlyActive);
    }

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){
        final View backgroundView = ((ClipViewHolder) viewHolder).clipBackground;
        final View foregroundView = ((ClipViewHolder) viewHolder).clipForeground;

        // TODO: should animate out instead. how?
        backgroundView.setRight(0);

        getDefaultUIUtil().clearView(foregroundView);
    }

    private static void drawBackground(RecyclerView.ViewHolder viewHolder, float dX, int actionState) {
        final View backgroundView = ((ClipViewHolder) viewHolder).clipBackground;

        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            //noinspection NumericCastThatLosesPrecision
            backgroundView.setRight((int) Math.max(dX, 0));
        }
    }
}

And in your layout file, define the foreground and background views:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/clipRow"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

        <View style="@style/Divider"/>

        <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/row_selector"
        >

        <RelativeLayout
            android:id="@+id/clipBackground"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:background="@color/swipe_bg"
            tools:layout_width="match_parent">

            <ImageView
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_alignParentLeft="true"
                android:layout_alignParentStart="true"
                android:layout_centerVertical="true"
                android:layout_marginLeft="12dp"
                android:layout_marginStart="12dp"
                android:focusable="false"
                android:src="@drawable/ic_delete_24dp"
                tools:ignore="ContentDescription"/>

        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/clipForeground"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingBottom="@dimen/clip_vertical_margin"
            android:paddingLeft="@dimen/clip_horizontal_margin"
            android:paddingRight="@dimen/clip_horizontal_margin"
            android:paddingTop="@dimen/clip_vertical_margin" >

            <CheckBox
                android:id="@+id/favCheckBox"
                android:layout_width="@dimen/view_image_size"
                android:layout_height="@dimen/view_image_size"
                android:layout_alignParentLeft="true"
                android:layout_alignParentStart="true"
                android:background="@android:color/transparent"
                android:button="@drawable/ic_star_outline_24dp"
                android:clickable="true"
                android:contentDescription="@string/content_favorite"
                />

             ...

        </RelativeLayout>

    </FrameLayout>

</LinearLayout>
Michael Updike
  • 644
  • 1
  • 6
  • 13
  • @Micheal Updike This doesn't work on API 10. To get it running on API 10 I had to port view.setRight() to LayoutParams and set the width of the view.The clipBackground works as expected, but the clipForeground view jumps to the top of the RecylerView. Any suggestions? – passerby Apr 06 '16 at 15:49
  • Hey @passerby did you got the solution? – Nand Aug 16 '16 at 15:16
  • 1
    @Michael Updike, your Code works Well , but how can I draw a background view for swipe left ? – VasFou Sep 12 '16 at 10:24
  • @Vasilisfoo I have same issue. It might be related to backgroundView.setRight – nLL Nov 11 '16 at 12:45
  • 2
    For this solution to work for me I've just removed the whole logic with the background from the simpleItemTouchCallback and in the layout the background is a match parent layout always visible behind the foreground. – Galya Nov 21 '16 at 11:55
  • Thanks for the answer, but what is ClipViewHolder, I cannot get anything about it online – Srivathsa Harish Venkataramana Jan 30 '17 at 00:20
  • @SrivathsaHarishVenkataramana that is just a concrete implementation of RecyclerView.ViewHolder – Michael Updike Jan 30 '17 at 01:32
  • 1
    Thanks for this detailed explanation. Very helpful. As a complement, I'd like to offer this, to let the background show when swiping left also: `if (dX > 0) backgroundView.setRight((int) dX); else backgroundView.setRight(backgroundView.getWidth() - (int) dX);` – MPelletier Jun 02 '17 at 03:33
  • This is the right answer, from the Android APIs perspective. – Marko Gajić Nov 29 '17 at 18:07
  • I was playing around with your answer and I got it to work for swipes from both sides but it still has a weird side effect on drag and drop – vatbub May 12 '18 at 23:56
  • @Michael Updike Using onMove() with ItemTouchHelper, I am able to drag and drop my CardViews in a RecyclerView list. However, the background does not move with the foreground. Do you have any advice or insight on how to fix? – AJW Aug 29 '18 at 19:36
4

Instead of draw over, I believe you should use viewType

Implements getItemViewType(int position) in adapter, returning different types, e.g. 0 for normal, 1 for swipped

and change onCreateViewHolder etc to return different layout on different viewType

Derek Fung
  • 8,171
  • 1
  • 25
  • 28
  • I'm pretty sure you are missing my point. This way I could modify the views that are used in a list, so some of them could look different than others, but how am I gonna use this in my case? I want to show my custom view, when ViewHolder is being swiped, `onCreateViewHolder` is not called then. – spoko Sep 13 '15 at 10:08
  • You have to know some basic operation on RecyclerView.Adapter. https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html there are various method named as `notifyItemXXXXX` and a method `notifyDataSetChanged`, whenever you changed the internal data of the adapter, you should call this methods, and RecyclerView will reload the views accordinginly. For example: For your case, after swipe, you should update the internal structure and mark the item as `removed`, and then you can call `notifyItemChanged(int position)` – Derek Fung Sep 13 '15 at 11:09
  • 2
    Yes, I know about these methods, but this is not my case. I don't want to do anything with the views **after** the swipe (they are working fine already, including deleting items on swipe), I want to change the view **during** swipe gesture, so the user can see the view **under** and know what is going to happen. Gmail inbox, swipe to archive - that's the example, what I want to achieve. – spoko Sep 13 '15 at 15:21
4

For people still finding this default, this is the simplest way.

A simple utility class to add a background, an icon and a label to a RecyclerView item while swiping it left or right.

enter image description here enter image description here

insert to Gradle

implementation 'it.xabaras.android:recyclerview-swipedecorator:1.2.1'

Override onChildDraw method of ItemTouchHelper class

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

    new RecyclerViewSwipeDecorator.Builder(MainActivity.this, c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
            .addBackgroundColor(ContextCompat.getColor(MainActivity.this, R.color.my_background))
            .addActionIcon(R.drawable.my_icon)
            .create()
            .decorate();

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

for more info -> https://github.com/xabaras/RecyclerViewSwipeDecorator

kelvin andre
  • 395
  • 5
  • 11
0

I managed to get this to work with the color and an image by extending the answer provided by Sanvywell at : Adding a colored background with text/icon under swiped row when using Android's RecyclerView

I won't recreate the full post (you can follow the link). In brief, I did not create a full view to render during the swipe, but only added a bitmap to the canvas, appropriately positioned to the RecyclerView item.

Community
  • 1
  • 1
HappyKatz
  • 399
  • 4
  • 10
  • I also wanted to draw the green background while the items below animate to their new positions after the swiped item is removed. For how to do it see my answer here http://stackoverflow.com/a/34687548/680050 – Nemanja Kovacevic Jan 08 '16 at 23:28
-2

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..

final 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;
                 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.

erik
  • 4,946
  • 13
  • 70
  • 120