RecyclerView进阶:使用ItemTouchHelper实现拖拽和侧滑删除

前言

现在RecyclerView的应用越来越广泛了,不同的应用场景需要其作出不同的改变。有时候我们可能需要实现侧滑删除的功能,比如知乎首页的侧滑删除,又或者长按Item进行拖动与其他Item进行位置的交换,但RecyclerView没有提供现成的API供我们操作,所幸SDK提供了ItemTouchHelper这样一个工具类帮助我们快速实现以上功能。不多说别的,我们来介绍一下ItemTouchHelper。

什么是ItemTouchHelper

This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.It works with a RecyclerView and a Callback class, which configures what type of interactions are enabled and also receives events when user performs these actions.Depending on which functionality you support, you should override onMove(RecyclerView, ViewHolder, ViewHolder) and / or onSwiped(ViewHolder, int).

以上是官方文档的介绍,ItemTouchHelper是一个工具类,可实现侧滑删除和拖拽移动,使用这个工具类需要RecyclerView和Callback。同时根据需要重写onMove和onSwiped方法。接下来就来讲述ItemTouchHelper的使用方法。

ItemTouchHelper基本使用方法

step.1新建一个接口,让Adapter实现之

从解耦的角度考虑,我们需要一个接口来实现Adapter和ItemTouchHelper之间涉及数据的操作,因为ItemTouchHelper在完成触摸的各种动画后,就要对Adapter的数据进行操作,比如侧滑删除操作,最后需要调用Adapter的notifyItemRemove()方法来移除该数据。因此我们可以把数据操作的部分抽象成一个接口方法,让ItemTouchHelper.Callback调用该方法即可。具体如下:
新建ItemTouchHelperAdapter:

public interface ItemTouchHelperAdapter {
    //数据交换
    void onItemMove(int fromPosition,int toPosition);
    //数据删除
    void onItemDissmiss(int position);
}

让我们的Adapter实现该接口:

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ItemTouchHelperAdapter {
    //数据
    private List<String> mData;
    ...
     @Override
    public void onItemMove(int fromPosition, int toPosition) {
        //交换位置
        Collections.swap(mData,fromPosition,toPosition);
        notifyItemMoved(fromPosition,toPosition);
    }

    @Override
    public void onItemDissmiss(int position) {
        //移除数据
        mData.remove(position);
        notifyItemRemoved(position);
    }

}

那么我们在ItemTouchHelper.Callback内直接调用接口的方法即可。

step.2新建类继承自ItemTouchHelper.Callback

从官方文档我们知道,使用ItemTouchHelper需要一个Callback,该Callback是ItemTouchHelper.Callback的子类,所以我们需要新建一个类比如SimpleItemTouchHelperCallback继承自ItemTouchHelper.Callback。我们可以重写其数个方法来实现我们的需求。我们先来看看ItemTouchHelper.Callback需要重写的几个常用的方法。
1、public int getMovementFlags(RecyclerView, RecyclerView.ViewHolder):该方法用于返回可以滑动的方向,比如说允许从右到左侧滑,允许上下拖动等。我们一般使用makeMovementFlags(int,int)或makeFlag(int, int)来构造我们的返回值。
例如:要使RecyclerView的Item可以上下拖动,同时允许从右到左侧滑,但不许允许从左到右的侧滑,我们可以这样写:

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;        //允许上下的拖动
        int swipeFlags = ItemTouchHelper.LEFT;   //只允许从右向左侧滑
        return makeMovementFlags(dragFlags,swipeFlags);
    }

2、public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
当用户拖动一个Item进行上下移动从旧的位置到新的位置的时候会调用该方法,在该方法内,我们可以调用Adapter的notifyItemMoved方法来交换两个ViewHolder的位置,最后返回true,表示被拖动的ViewHolder已经移动到了目的位置。所以,如果要实现拖动交换位置,可以重写该方法(前提是支持上下拖动):

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        //onItemMove是接口方法
        mAdapter.onItemMove(viewHolder.getAdapterPosition(),target.getAdapterPosition());  
        return true;
    }

3、public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)
当用户左右滑动Item达到删除条件时,会调用该方法,一般手指触摸滑动的距离达到RecyclerView宽度的一半时,再松开手指,此时该Item会继续向原先滑动方向滑过去并且调用onSwiped方法进行删除,否则会反向滑回原来的位置。在该方法内部我们可以这样写:

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        //onItemDissmiss是接口方法
        mAdapter.onItemDissmiss(viewHolder.getAdapterPosition());
    }

如果在onSwiped方法内我们没有进行任何操作,即不删除已经滑过去的Item,那么就会留下空白的地方,因为实际上该ItemView还占据着该位置,只是移出了我们的可视范围内罢了。

4、public boolean isLongPressDragEnabled():该方法返回true时,表示支持长按拖动,即长按ItemView后才可以拖动,我们遇到的场景一般也是这样的。默认是返回true。

5、public boolean boolean isItemViewSwipeEnabled():该方法返回true时,表示如果用户触摸并左右滑动了View,那么可以执行滑动删除操作,即可以调用到onSwiped()方法。默认是返回true。

6、public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState):从静止状态变为拖拽或者滑动的时候会回调该方法,参数actionState表示当前的状态。

7、public void clearView(RecyclerView recyclerView, ViewHolder viewHolder):当用户操作完毕某个item并且其动画也结束后会调用该方法,一般我们在该方法内恢复ItemView的初始状态,防止由于复用而产生的显示错乱问题。

8、public void onChildDraw(...):我们可以在这个方法内实现我们自定义的交互规则或者自定义的动画效果。
那么完整的SimpleItemTouchHelperCallback文件是这样的:

public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback{

    private ItemTouchHelperAdapter mAdapter;

    public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter){
        mAdapter = adapter;
    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        int swipeFlags = ItemTouchHelper.LEFT;
        return makeMovementFlags(dragFlags,swipeFlags);
    }

    @Override
    public boolean isLongPressDragEnabled() {
        return true;
    }

    @Override
    public boolean isItemViewSwipeEnabled() {
        return true;
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        mAdapter.onItemMove(viewHolder.getAdapterPosition(),target.getAdapterPosition());
        return true;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        mAdapter.onItemDissmiss(viewHolder.getAdapterPosition());
    }
}

step.3为RecycleView添加ItemTouchHelper

上面我们修改了Adapter和新建了ItemTouchHelper.Callback的子类,接下来我们要为RecyclerView添加ItemTouchHelper:

    //先实例化Callback
    ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(myAdapter);
    //用Callback构造ItemtouchHelper
    ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
    //调用ItemTouchHelper的attachToRecyclerView方法建立联系
    touchHelper.attachToRecyclerView(mRecyclerView);

经过以上步骤,我们已经实现了Item的拖拽和侧滑删除功能了,看一下效果:


拖拽和侧滑.gif

自定义侧滑动画

有时候我们对默认的动画效果可能不满意,需要自己实现想要的动画效果,ItemTouchHelper.Callback提供的onChildDraw方法可以让我们很方便地实现想要的效果。以下带来一种自定义的实现效果,当做抛砖引玉,让大家熟悉自定义效果的运用。先来看看要实现的效果:


自定义侧滑.gif

该效果是比较常见的,用户向左滑动Item的时候,一开始提示的是“左滑删除”,滑动到一定距离后,显示删除的图标,并且随着滑动距离的增加该图标不断变大,达到最大后用户松开手指,该Item被删除。
接下来我们来分析一下怎样实现以上的效果:
首先,要想左滑出现一个删除的方块,可以在LinearLayout放一个这样的“方块”,让它与Item水平并排排列,以下是布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:orientation="horizontal">

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:background="#ffffff"
        android:layout_marginLeft="4dp"
        android:layout_marginRight="4dp"
        android:layout_marginBottom="4dp"
        app:cardCornerRadius="1dp"
        app:elevation="1dp"
        app:contentPadding="1dp">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ffffff">
            <TextView
                android:id="@+id/item"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="22sp"
                android:padding="4dp"
                android:layout_centerInParent="true"/>

        </RelativeLayout>
    </android.support.v7.widget.CardView>

    <FrameLayout
        android:layout_width="100dp"
        android:layout_height="match_parent"
        android:layout_marginRight="4dp"
        android:layout_marginBottom="4dp"
        android:background="#f33213">

        <ImageView
            android:id="@+id/iv_img"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_gravity="center"
            android:src="@mipmap/ic_eye_72"
            android:visibility="invisible"/>
        <TextView
            android:id="@+id/tv_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="左滑删除"
            android:textSize="18sp"
            android:textColor="#ffffff"
            android:layout_gravity="center"/>
    </FrameLayout>
</LinearLayout>

布局文件修改后,我们尝试来滑动一下,发现后面的删除方块并不会出现,这是因为默认的滑动方式是setTranslationX(int),即是对整个View的滑动,所以无论我们怎样滑动,都不会出现删除方块。因此,我们要改变一个种滑动方式,比如使用scrollTo(int,int),这种是对View的内容的滑动,所以随着左滑,item会向左滑去,而位于右方的方块自然也就出现了。
接着,我们考虑该“删除眼睛”的图标是怎样从小变大的,这个实现也比较简单,只要根据滑动的距离对该ImageView的LayoutParams.width进行改变就行了,不过要注意限制大小,否则过大会造成图片的失真。当滑动距离等于RecyclerView宽度的一半时,此时松开手会使Item删除,那么我们可以在该滑动距离达到该值时时“眼睛”变得最大,此时可以达到良好的交互效果,提示了用户无需继续滑动即可删除该Item了。
最后我们要考虑的是:在删除了Item或者没删除而滑回原来的位置后,我们要把所做的改变重置一下,否则,会由于RecyclerView的复用而导致其他位置的ViewHolder与当前的ViewHolder所做的改变一样,即造成显示的错误。我们可以在clearView()方法内重置改变,这样就能解决因复用而导致的显示问题了。
最后我们来看看SimpleItemTouchHelperCallback的代码:

public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback{

    //省略上面的代码....

    //限制ImageView长度所能增加的最大值
    private double ICON_MAX_SIZE = 50;
    //ImageView的初始长宽
    private int fixedWidth = 150;

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        //重置改变,防止由于复用而导致的显示问题
        viewHolder.itemView.setScrollX(0);
        ((MyAdapter.NormalItem)viewHolder).tv.setText("左滑删除");
        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) ((MyAdapter.NormalItem) viewHolder).iv.getLayoutParams();
        params.width = 150;
        params.height = 150;
        ((MyAdapter.NormalItem) viewHolder).iv.setLayoutParams(params);
        ((MyAdapter.NormalItem) viewHolder).iv.setVisibility(View.INVISIBLE);
    }

    @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){
            //如果dX小于等于删除方块的宽度,那么我们把该方块滑出来
            if (Math.abs(dX) <= getSlideLimitation(viewHolder)){
                viewHolder.itemView.scrollTo(-(int) dX,0);
            }
            //如果dX还未达到能删除的距离,此时慢慢增加“眼睛”的大小,增加的最大值为ICON_MAX_SIZE
            else if (Math.abs(dX) <= recyclerView.getWidth() / 2){
                double distance = (recyclerView.getWidth() / 2 -getSlideLimitation(viewHolder));
                double factor = ICON_MAX_SIZE / distance;
                double diff =  (Math.abs(dX) - getSlideLimitation(viewHolder)) * factor;
                if (diff >= ICON_MAX_SIZE)
                    diff = ICON_MAX_SIZE;
                ((MyAdapter.NormalItem)viewHolder).tv.setText("");   //把文字去掉
                ((MyAdapter.NormalItem) viewHolder).iv.setVisibility(View.VISIBLE);  //显示眼睛
                FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) ((MyAdapter.NormalItem) viewHolder).iv.getLayoutParams();
                params.width = (int) (fixWidth + diff);
                params.height = (int) (fixWidth + diff);
                ((MyAdapter.NormalItem) viewHolder).iv.setLayoutParams(params);
            }
        }else {
            //拖拽状态下不做改变,需要调用父类的方法
            super.onChildDraw(c,recyclerView,viewHolder,dX,dY,actionState,isCurrentlyActive);
        }
    }

    /**
     * 获取删除方块的宽度
     */
    public int getSlideLimitation(RecyclerView.ViewHolder viewHolder){
        ViewGroup viewGroup = (ViewGroup) viewHolder.itemView;
        return viewGroup.getChildAt(1).getLayoutParams().width;
    }
}

好了,到目前为止,自定义效果介绍完毕,读者可以根据需求来实现多样化的效果。最后,感谢你的阅读,如有错误欢迎指出~

参考文章
RecyclerView的拖动和滑动 第一部分 :基本的ItemTouchHelper示例

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容