当前位置:网站首页>RecyclerView高级使用(一)-侧滑删除的简单实现

RecyclerView高级使用(一)-侧滑删除的简单实现

2022-04-23 14:06:00 森之千手

前言

做安卓开发的同学,对RecyclerView一定都不陌生。早在它问世之前,我们安卓猿猿们实现列表或者表格常用的只用ListView和GridView。由于早期安卓开发相关的sdk说明文档的中文版不是很完善,很多用法实际上是很有问题的。就比如ListView,如果不用ViewHolder也能跑,但是为什么用,在当时也很少有人能完全讲清楚。RecyclerView的问世给这种不明了带来了新的生机。简单来说,这个新控件可以认为是自带ViewHolder的对ListView和GridView的集大成者。深一点来说,对列表缓存,列表行为的自定义操作进行了极大的扩展。利用这些扩展功能,我们能轻松地实现很多在ListView时代要写很多代码才能实现地功能。接下来的第一课,我们就来探讨一下,如何简单实现一个侧滑删除操作。

先看看我们要实现地效果:
RecyclerView侧滑删除简单实现
页面比较简单,就是一个列表,然后支持侧滑删除。

实现这个效果需要用到一个叫做ItemTouchHelper的辅助类。下面就是这个类的官方英文介绍。

androidx.recyclerview.widget.ItemTouchHelper @Contract(pure = true) 
public ItemTouchHelper(@NonNull ItemTouchHelper.Callback callback)
Creates an ItemTouchHelper that will work with the given Callback.
You can attach ItemTouchHelper to a RecyclerView via attachToRecyclerView(RecyclerView). Upon attaching, it will add an item decoration, an onItemTouchListener and a Child attach / detach listener to the RecyclerView.

Params:
callback – The Callback which controls the behavior of this touch helper.

大致就是说这个类构造的时候需要传入一个CallBack参数,然后通过attachToRecyclerView这个方法和RecyclerView绑定在一起。

既然需要一个CallBack作为构造参数,那么我们就先自定义一个CallBack:

public class SlideDeleteHelperCallBack extends ItemTouchHelper.Callback {
    
    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
    
        return 0;
    }

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

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
    

	}
}

我们自定义一个SlideDeleteHelperCallBack类继承ItemTouchHelper.Callbak,默认必须实现三个抽象方法:
ItemTouchHelper.CallBack
首先看看getMovementFlags

/** * Should return a composite flag which defines the enabled move directions in each state * (idle, swiping, dragging). * <p> * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int, * int)} * or {@link #makeFlag(int, int)}. * <p> * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next * 8 bits are for SWIPE state and third 8 bits are for DRAG state. * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in * {@link ItemTouchHelper}. * <p> * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to * swipe by swiping RIGHT, you can return: * <pre> * makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT); * </pre> * This means, allow right movement while IDLE and allow right and left movement while * swiping. * * @param recyclerView The RecyclerView to which ItemTouchHelper is attached. * @param viewHolder The ViewHolder for which the movement information is necessary. * @return flags specifying which movements are allowed on this ViewHolder. * @see #makeMovementFlags(int, int) * @see #makeFlag(int, int) */
  public abstract int getMovementFlags(@NonNull RecyclerView recyclerView,
          @NonNull ViewHolder viewHolder);

讲得比较详细,中文简单翻译就是,该方法需要返回一个int类型的标记量。而构造这个标记量可以使用makeMovementFlags这个辅助方法实现:

/** * Convenience method to create movement flags. * <p> * For instance, if you want to let your items be drag & dropped vertically and swiped * left to be dismissed, you can call this method with: * <code>makeMovementFlags(UP | DOWN, LEFT);</code> * * @param dragFlags The directions in which the item can be dragged. * @param swipeFlags The directions in which the item can be swiped. * @return Returns an integer composed of the given drag and swipe flags. */
 public static int makeMovementFlags(int dragFlags, int swipeFlags) {
    
     return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
             | makeFlag(ACTION_STATE_SWIPE, swipeFlags)
             | makeFlag(ACTION_STATE_DRAG, dragFlags);
 }

而makeMovementFlags,实际需要传入的是一个dragFlags(拖拽标记)和一个swipeFlags(滑动标记)。因此,我们就能很清楚地理解getMovementFlags实际需要的是拖拽和滑动的标记量。至于为什么需要采用这种方式返回,首先,单纯用一个数组返回两种类型的值,我觉得也是完全可行。不过,采用位或运算的话,在执行效率上会更高效,因为,安卓源码层里面很多这种多个选值的(例如Gravity)都是采用位或运算,而且代码还显得简洁。
再回过来,在我们当前的这个实例里面,我们其实只需要侧滑,不需要拖拽。那么具体传值该怎么传呢。

@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
    
    return makeMovementFlags(0,ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT);
}

我们可以写成这样,这也是网上比较流行的写法。第一个参数dragFlags传0,第二个swipeFlags传LEFTRIGHT的或运算值,表明可以左划和右划(当然可以只传其一,那样就只有一个方向能划)。但是第一个参数为什么要传0,很多地方都没说明原因。怎么去找答案呢?由于后面的参数都是常量构成,那势必前面的参数理应也是常量。我们点到后面的常量参数源码,看看:
常量
可以看到,其实是有个0的常量的,叫做ACTION_STATE_IDLE,从注释就能看出,该常量表明是一种静止状态,不会有任何操作。自然,传0就不会实现拖拽操作了,因此,比较完美的写法应该改为:

@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
    
    return makeMovementFlags(ItemTouchHelper.ACTION_STATE_IDLE,ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT);
}

接下来我们再看看三个方法中的onMove方法,找到源码:

/** * Called when ItemTouchHelper wants to move the dragged item from its old position to * the new position. * <p> * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved * to the adapter position of {@code target} ViewHolder * ({@link ViewHolder#getAdapterPosition() * ViewHolder#getAdapterPosition()}). * <p> * If you don't support drag & drop, this method will never be called. * * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. * @param viewHolder The ViewHolder which is being dragged by the user. * @param target The ViewHolder over which the currently active item is being * dragged. * @return True if the {@code viewHolder} has been moved to the adapter position of * {@code target}. * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int) */
 public abstract boolean onMove(@NonNull RecyclerView recyclerView,
         @NonNull ViewHolder viewHolder, @NonNull ViewHolder target);

可以看到,该方法主要是用来操作拖拽时,条目移动时的一些操作,如果返回true的话,还会走onMoved的这个方法。但是我们这里只是侧滑,不需要拖拽,因此,该方法,我们可以略过。

最后来看看onSwiped的这个方法,也是先看看源码:

/** * Called when a ViewHolder is swiped by the user. * <p> * If you are returning relative directions ({@link #START} , {@link #END}) from the * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method * will also use relative directions. Otherwise, it will use absolute directions. * <p> * If you don't support swiping, this method will never be called. * <p> * ItemTouchHelper will keep a reference to the View until it is detached from * RecyclerView. * As soon as it is detached, ItemTouchHelper will call * {@link #clearView(RecyclerView, ViewHolder)}. * * @param viewHolder The ViewHolder which has been swiped by the user. * @param direction The direction to which the ViewHolder is swiped. It is one of * {@link #UP}, {@link #DOWN}, * {@link #LEFT} or {@link #RIGHT}. If your * {@link #getMovementFlags(RecyclerView, ViewHolder)} * method * returned relative flags instead of {@link #LEFT} / {@link #RIGHT}; * `direction` will be relative as well. ({@link #START} or {@link * #END}). */
 public abstract void onSwiped(@NonNull ViewHolder viewHolder, int direction);

这段注释大概意思有两个:
1、如果不支持swipe,那么该方法永远不会被调用。是否支持swipe,可以复写:

@Override
public boolean isItemViewSwipeEnabled() {
    
    return super.isItemViewSwipeEnabled();
}

来进行控制,默认返回true,表明支持swipe
2、该方法的direction的值和你在前面的getMovementFlags中返回的值相关,如果你在前面返回的是类似START和END这种相对定位的方向,这里拿到的也是相对定位的方向,否则返回左上右下的绝对方向。

那么,我们所需侧滑操作的核心就在这个onSwiped方法里了。
到这里,我们先想想RecyclerView的设计模式,观察者模式。视图和数据分开,通过数据的改变去驱动视图的改变(notifyXXchanged)。在ListView时代,驱动视图更新只有notifyDataSetChanged,这种更新是全量型的。到了RecyclerView时代,扩展出了局部更新,例如notifyXXRemoved,notifyXXInserted。。。
此时我们再回过头看看我们那个自定义的ItemTouchHelper的callBack:

public class SlideDeleteHelperCallBack extends ItemTouchHelper.Callback {
    
    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
    
        return 0;
    }

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

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
    

	}
}

onSwiped方法只提供了一个viewHolder和一个direction,那么我们能通过这二者拿到其所属父视图和数据源列表吗?原生的ViewHolder是不行的(后期我们可以自行封装实现),因此我们需要传入一个RecyclerView和一个数据源List。看看第一个动图,每个条目的对象就是一个图片和一个文本,因此我们可以建模:

public class RecyclerItem {
    
    private int icon;
    private String text;

    public RecyclerItem() {
    
    }

    public RecyclerItem(@DrawableRes int icon, String text) {
    
        this.icon = icon;
        this.text = text;
    }

    public int getIcon() {
    
        return icon;
    }

    public void setIcon(@DrawableRes int icon) {
    
        this.icon = icon;
    }

    public String getText() {
    
        return text;
    }

    public void setText(String text) {
    
        this.text = text;
    }

    @Override
    public String toString() {
    
        return "RecyclerItem{" +
                "icon=" + icon +
                ", text='" + text + '\'' +
                '}';
    }
}

当然,为了日志打印方便,我们还复写了toString方法。这样的话,就可以创建构造函数了:

private RecyclerView recyclerView;
private List<RecyclerItem> recyclerItemList;

public SlideDeleteHelperCallBack(RecyclerView recyclerView, List<RecyclerItem> recyclerItemList) {
    
    this.recyclerView = recyclerView;
    this.recyclerItemList = recyclerItemList;
}

onSwiped方法里面我们可以这样写:

@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
    
   recyclerItemList.remove(viewHolder.getAdapterPosition());
   recyclerView.getAdapter().notifyItemRemoved(viewHolder.getAdapterPosition());
}

先从数据源上删除数据,再通知从从视图层级上删除数据。从而达到数据的改变来驱动视图。
但是这里实际上是有坑的。上面的写法也是大多数网上文章的写法。因为单纯从侧滑效果上来看,是已经达到了。但是如果涉及到点击事件,那么就会出现点击时的位置不正确的问题。*具体的实现修复可查看RecyclerView细节研究-RecyclerView点击错位问题的探讨与修复。至此,ItemTouchHelper.CallBack部分我们就写完了。
现在我们看看连接callBack和RecyclerView的代码:

public class SlideDeleteActivity extends BaseActivity {
    
    private RecyclerView recycler;
    private List<RecyclerItem> list;
    private SimpleRecyclerListAdapter adapter;
    private ItemTouchHelper itemTouchHelper;

    @Override
    protected int setLayoutId() {
    
        return R.layout.activity_slide_delete;
    }

    @Override
    protected int setToolBarId() {
    
        return R.id.toolbar;
    }

    @Override
    protected void initView(Bundle savedInstanceState) {
    
        recycler = findViewById(R.id.recycler);
    }

    @Override
    protected void initEvents(Bundle savedInstanceState) {
    
        list = DataFactory.generateRecyclerItemList();
        adapter = new SimpleRecyclerListAdapter(list);
        adapter.setOnItemCLickListener(new SimpleRecyclerListAdapter.OnItemCLickListener() {
    
            @Override
            public void onItemClick(RecyclerItem recyclerItem, SimpleRecyclerViewHolder holder, int position) {
    
                toastShort("点击了:" + recyclerItem + " 位置:" + position);
            }
        });
        recycler.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        recycler.setAdapter(adapter);
        recycler.addItemDecoration(new GapDecoration());

        SlideDeleteHelperCallBack callBack = new SlideDeleteHelperCallBack(recycler, list);
        callBack.setOnSwipedListener(new SlideDeleteHelperCallBack.OnSwipedListener() {
    
            @Override
            public void onSwiped(RecyclerView.ViewHolder viewHolder, RecyclerItem deletedItem, int deletedPos) {
    
                toastShort("删除了:" + deletedItem + " 位置:" + deletedPos);
            }
        });
        itemTouchHelper = new ItemTouchHelper(callBack);
        itemTouchHelper.attachToRecyclerView(recycler);
    }
}

因为不想写重复代码,封装了一下基类。还设置了一下自定义的Decoration,后面有空的话会专门写一篇文章介绍Decoration。然后就是最后一行attachToRecyclerView,这样就能简单实现第一个动图效果了。
当然,只是简单实现,后面还会推出如何魔改为QQ侧滑效果。

交流邮箱:[email protected]
源码地址:Github

版权声明
本文为[森之千手]所创,转载请带上原文链接,感谢
https://blog.csdn.net/cjs1534717040/article/details/113857718