26

RecyclerView by default, does come with a nice deletion animation, as long as you setHasStableIds(true) and provide correct implementation on getItemId.

Recently, I had added divider into RecyclerView via https://stackoverflow.com/a/27037230/72437

The outcome looks as following

https://www.youtube.com/watch?v=u-2kPZwF_0w

https://youtu.be/c81OsFAL3zY (To make the dividers more visible when delete animation played, I temporary change the RecyclerView background to red)

The dividers are still visible, when deletion animation being played.

However, if I look at GMail example, when deletion animation being played, divider lines are no longer visible. They are being covered a solid color area.

https://www.youtube.com/watch?v=cLs7paU-BIg

May I know, how can I achieve the same effect as GMail, by not showing divider lines, when deletion animation played?

Community
  • 1
  • 1
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875

3 Answers3

35

The solution is fairly easy. To animate a decoration, you can and should use view.getTranslation_() and view.getAlpha(). I wrote a blog post some time ago on this exact issue, you can read it here.

Translation and fading off

The default layout manager will fade views out (alpha) and translate them, when they get added or removed. You have to account for this in your decoration.

The idea is simple:

However you draw your decoration, apply the same alpha and translation to your drawing by using view.getAlpha() and view.getTranslationY().

Following your linked answer, it would have to be adapted like the following:

// translate
int top = child.getBottom() + params.bottomMargin + view.getTranslationY();
int bottom = top + mDivider.getIntrinsicHeight();

// apply alpha
mDivider.setAlpha((int) child.getAlpha() * 255f);
mDivider.setBounds(left + view.getTranslationX(), top,
        right + view.getTranslationX(), bottom);
mDivider.draw(c);

A complete sample

I like to draw things myself, since I think drawing a line is less overhead than layouting a drawable, this would look like the following:

public class SeparatorDecoration extends RecyclerView.ItemDecoration {

    private final Paint mPaint;
    private final int mAlpha;

    public SeparatorDecoration(@ColorInt int color, float width) {
        mPaint = new Paint();
        mPaint.setColor(color);
        mPaint.setStrokeWidth(width);
        mAlpha = mPaint.getAlpha();
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();

        // we retrieve the position in the list
        final int position = params.getViewAdapterPosition();

        // add space for the separator to the bottom of every view but the last one
        if (position < state.getItemCount()) {
            outRect.set(0, 0, 0, (int) mPaint.getStrokeWidth()); // left, top, right, bottom
        } else {
            outRect.setEmpty(); // 0, 0, 0, 0
        }
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        // a line will draw half its size to top and bottom,
        // hence the offset to place it correctly
        final int offset = (int) (mPaint.getStrokeWidth() / 2);

        // this will iterate over every visible view
        for (int i = 0; i < parent.getChildCount(); i++) {
            final View view = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();

            // get the position
            final int position = params.getViewAdapterPosition();

            // and finally draw the separator
            if (position < state.getItemCount()) {
                // apply alpha to support animations
                mPaint.setAlpha((int) (view.getAlpha() * mAlpha));

                float positionY = view.getBottom() + offset + view.getTranslationY();
                // do the drawing
                c.drawLine(view.getLeft() + view.getTranslationX(),
                        positionY,
                        view.getRight() + view.getTranslationX(),
                        positionY,
                        mPaint);
            }
        }
    }
}
David Medenjak
  • 33,993
  • 14
  • 106
  • 134
7

Firstly, sorry for the massive answer size. However, I felt it necessary to include my entire test Activity so that you can see what I have done.

The issue

The issue that you have, is that the DividerItemDecoration has no idea of the state of your row. It does not know whether the item is being deleted.

For this reason, I made a POJO that we can use to contain an integer (that we use as both an itemId and a visual representation and a boolean indicating that this row is being deleted or not.

When you decide to delete entries (in this example adapter.notifyItemRangeRemoved(3, 8);), you must also set the associated Pojo to being deleted (in this example pojo.beingDeleted = true;).

The position of the divider when beingDeleted, is reset to the colour of the parent view. In order to cover up the divider.

I am not very fond of using the dataset itself to manage the state of its parent list. There is perhaps a better way.

The result visualized

Removing items and their dividers

The Activity:

public class MainActivity extends AppCompatActivity {
    private static final int VERTICAL_ITEM_SPACE = 8;

    private List<Pojo> mDataset = new ArrayList<Pojo>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        for(int i = 0; i < 30; i++) {
            mDataset.add(new Pojo(i));
        }

        final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);

        recyclerView.addItemDecoration(new VerticalSpaceItemDecoration(VERTICAL_ITEM_SPACE));
        recyclerView.addItemDecoration(new DividerItemDecoration(this));

        RecyclerView.ItemAnimator ia = recyclerView.getItemAnimator();
        ia.setRemoveDuration(4000);

        final Adapter adapter = new Adapter(mDataset);
        recyclerView.setAdapter(adapter);

        (new Handler(Looper.getMainLooper())).postDelayed(new Runnable() {
            @Override
            public void run() {
                int index = 0;
                Iterator<Pojo> it = mDataset.iterator();
                while(it.hasNext()) {
                    Pojo pojo = it.next();

                    if(index >= 3 && index <= 10) {
                        pojo.beingDeleted = true;
                        it.remove();
                    }

                    index++;
                }

                adapter.notifyItemRangeRemoved(3, 8);
            }
        }, 2000);
    }

    public class Adapter extends RecyclerView.Adapter<Holder> {
        private List<Pojo> mDataset;

        public Adapter(@NonNull final List<Pojo> dataset) {
            setHasStableIds(true);
            mDataset = dataset;
        }

        @Override
        public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_cell, parent, false);
            return new Holder(view);
        }

        @Override
        public void onBindViewHolder(final Holder holder, final int position) {
            final Pojo data = mDataset.get(position);

            holder.itemView.setTag(data);
            holder.textView.setText("Test "+data.dataItem);
        }

        @Override
        public long getItemId(int position) {
            return mDataset.get(position).dataItem;
        }

        @Override
        public int getItemCount() {
            return mDataset.size();
        }
    }

    public class Holder extends RecyclerView.ViewHolder {
        public TextView textView;

        public Holder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.text);
        }
    }

    public class Pojo {
        public int dataItem;
        public boolean beingDeleted = false;

        public Pojo(int dataItem) {
            this.dataItem = dataItem;
        }
    }

    public class DividerItemDecoration extends RecyclerView.ItemDecoration {

        private final int[] ATTRS = new int[]{android.R.attr.listDivider};

        private Paint mOverwritePaint;
        private Drawable mDivider;

        /**
         * Default divider will be used
         */
        public DividerItemDecoration(Context context) {
            final TypedArray styledAttributes = context.obtainStyledAttributes(ATTRS);
            mDivider = styledAttributes.getDrawable(0);
            styledAttributes.recycle();
            initializePaint();
        }

        /**
         * Custom divider will be used
         */
        public DividerItemDecoration(Context context, int resId) {
            mDivider = ContextCompat.getDrawable(context, resId);
            initializePaint();
        }

        private void initializePaint() {
            mOverwritePaint = new Paint();
            mOverwritePaint.setColor(ContextCompat.getColor(MainActivity.this, android.R.color.background_light));
        }

        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();

            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);

                RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();

                int top = child.getBottom() + params.bottomMargin;
                int bottom = top + mDivider.getIntrinsicHeight();

                Pojo item = (Pojo) child.getTag();
                if(item.beingDeleted) {
                    c.drawRect(left, top, right, bottom, mOverwritePaint);
                } else {
                    mDivider.setBounds(left, top, right, bottom);
                    mDivider.draw(c);
                }

            }
        }
    }

    public class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {

        private final int mVerticalSpaceHeight;

        public VerticalSpaceItemDecoration(int mVerticalSpaceHeight) {
            this.mVerticalSpaceHeight = mVerticalSpaceHeight;
        }

        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
                                   RecyclerView.State state) {
            outRect.bottom = mVerticalSpaceHeight;
        }
    }
}

The Activity Layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:background="@android:color/background_light"
    tools:context="test.dae.myapplication.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

The RecyclerView "row" Layout

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:id="@+id/text"
          android:padding="8dp">

</TextView>
Knossos
  • 15,802
  • 10
  • 54
  • 91
1

I think the ItemDecorator you use to draw a divider after every row is messing things up when swipe to delete is performed.

Instead of Using ItemDecorator to draw a Divider in a recyclerview, add a view at the end of your RecyclerView child layout design.which will draw a divider line like ItemDecorator.

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
 <!-- child layout Design !-->

 <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@android:color/darker_gray"
        android:layout_gravity="bottom"
        />
 </Linearlayout>
HourGlass
  • 1,805
  • 1
  • 15
  • 29
  • 1
    This is by far the easiest solution, as far as I can see. – Marc Feb 05 '19 at 18:41
  • This is not the recommended way to add separators to recyclerViews, but it saves you some time and trouble as the one described in the question. However, I think it's worth going the extra mile to implement it with `ItemDecorators`. – Lucas P. Feb 28 '20 at 17:26