Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - Swipe to dismiss RecyclerView with animation on the background

As far as I understood, one possibility to implement swipe-to-dismiss for RecyclerView with a background below the swiped item (as in many google apps) is to implement a simple callback for the ItemTouchHelper and draw the background in the method onChildDraw.

This is my implementation:

ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
    // create objects once and avoid allocating them in the onChildDraw method
    Drawable background;
    Drawable icon;
    int iconMargin;
    boolean initialized;

    private void init() {
        background = new ColorDrawable(Color.RED);
        icon = ContextCompat.getDrawable(mAppContext, R.drawable.ic_delete_white_24dp);
        iconMargin = (int) mAppContext.getResources().getDimension(R.dimen.fab_margin);
        initialized = true;
    }

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

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
        // remove swiped item from the list and notify the adapter
        int position = viewHolder.getAdapterPosition();
        mItemList.remove(position);
        mRecyclerViewAdapter.notifyDataSetChanged();
    }

    @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 (dX > 0) {
                View itemView = viewHolder.itemView;
                // if viewHolder has been swiped away, don't do anything
                if (viewHolder.getAdapterPosition() == -1) {
                    return;
                }
                if (!initialized) {
                    init();
                }
                // draw background
                int dXSwipe = (int) (dX * 1.05); // increase slightly dX to avoid view flickering, compensating loss of precision due to int conversion 
                background.setBounds(itemView.getLeft(), itemView.getTop(),
                itemView.getLeft() + dXSwipe, itemView.getBottom());
                background.draw(c);
                // draw icon
                int top = (itemView.getTop() + itemView.getBottom() - icon.getIntrinsicHeight()) / 2;
                int left = itemView.getLeft() + iconMargin;
                icon.setBounds(left, top, left + icon.getIntrinsicWidth(), top + icon.getIntrinsicHeight());
                icon.draw(c);
            }
        }
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        }
    };
    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
    itemTouchHelper.attachToRecyclerView(mRecyclerView);

Now, the question is how to animate the views in the background below. An example of this animation can be taken from the google calendar: when events or reminders are swiped, the icon on the left is scaled up accordingly to the amount of the horizontal displacement.

Has anybody idea how to achieve that? Would it be necessary a different approach, maybe with two views in the ViewHolder one on each other as proposed here RecyclerView Swipe with a view below it?

like image 259
Rosso Avatar asked Aug 09 '17 20:08

Rosso


1 Answers

I found out how to do it.

For those who are interested, the solution is to use two views (foreground and background) and animate the background as the swipe progresses.

The layout:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    android:background="@color/item_delete_background">

    <ImageView
        android:id="@+id/item_delete_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="@dimen/fab_margin"
        android:layout_marginStart="@dimen/fab_margin"
        app:srcCompat="@drawable/ic_delete_white_24dp" />

    <android.support.constraint.ConstraintLayout
        android:id="@+id/item_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/gray_bg">

        <!--recyclerview item layout-->

    </android.support.constraint.ConstraintLayout>

</FrameLayout>

The view holder:

class MyViewHolder extends RecyclerView.ViewHolder {
    private ConstraintLayout mItemLayout;
    private ImageView mItemDeleteIcon;

    MyViewHolder(View v) {
        super(v);
        mItemLayout = (ConstraintLayout) v.findViewById(R.id.item_layout);
        mEventDeleteIcon = (ImageView) v.findViewById(R.id.item_delete_icon);
    }

    View getViewToSwipe() {
        return mItemLayout;
    }

    View getViewToAnimate() {
        return mItemDeleteIcon;
    }
}

And the ItemTouchHelper callback:

ItemTouchHelper.SimpleCallback mItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {

    // override methods

    public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
                                float dX, float dY,
                                int actionState, boolean isCurrentlyActive) {
        // get the view which is currently swiped
        ConstraintLayout itemLayout = (ConstraintLayout) ((MyViewHolder) viewHolder).getViewToSwipe();
        // calculate relative horizontal displacement
        // with proportion dXRelative : 1 = dX : (layoutWidth / 3)
        float dXRelative = dX / itemLayout.getWidth() * 3;
        // check size boundaries
        if (dXRelative > 1) {
            dXRelative = 1;
        }
        if (dXRelative < 0) {
            dXRelative = 0;
        }
        // animate the icon with scaling on both dimensions
        ((MyViewHolder) viewHolder).getViewToAnimate().animate().scaleX(dXRelative).scaleY(dXRelative).setDuration(0).start();
        // call draw over
        getDefaultUIUtil().onDrawOver(c, recyclerView, ((MyViewHolder) viewHolder).getViewToSwipe(), dX, dY, actionState, isCurrentlyActive);
    }
};

The implementation of the basic behavior (foreground/background in the view holder) was done following this post: Use ItemTouchHelper for Swipe-To-Dismiss with another View displayed behind the swiped out.

Hope this might be useful for someone.

like image 67
Rosso Avatar answered Nov 16 '22 07:11

Rosso