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

前言

做安卓开发的同学,对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侧滑效果。

交流邮箱:chenjunsen@outlook.com
源码地址:Github

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RecyclerView的侧菜单可以通过ItemTouchHelper类来实现。 首先,需要创建一个实现ItemTouchHelper.Callback的类,重写其中的方法,用于处理拖拽和侧等操作。在其中,我们需要实现onSwiped方法,用于处理RecyclerView中的侧操作。 可以通过实现onChildDraw方法来绘制侧菜单,例如使用Canvas绘制背景和图标等。然后在onSwiped方法中,通过ViewHolder.getAdapterPosition()获取当前侧的位置,然后再调用Adapter的remove方法来删除对应的数据。最后,需要在Adapter中实现onCreateViewHolder方法,在其中绑定侧菜单的布局和操作。 下面是一个简单的示例代码: ```java public class SwipeController extends ItemTouchHelper.Callback { private RecyclerView.Adapter adapter; public SwipeController(RecyclerView.Adapter adapter) { this.adapter = adapter; } @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { return makeMovementFlags(0, ItemTouchHelper.LEFT); } @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) { int position = viewHolder.getAdapterPosition(); adapter.notifyItemRemoved(position); adapter.notifyItemRangeChanged(position, adapter.getItemCount()); } @Override public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { View itemView = viewHolder.itemView; Paint p = new Paint(); if (dX < 0) { p.setColor(Color.RED); RectF background = new RectF((float)itemView.getRight() + dX, (float)itemView.getTop(), (float)itemView.getRight(), (float)itemView.getBottom()); c.drawRect(background, p); Drawable icon = ContextCompat.getDrawable(adapter.getContext(), R.drawable.ic_delete); int iconWidth = icon.getIntrinsicWidth(); int iconHeight = icon.getIntrinsicHeight(); int left = itemView.getRight() - iconWidth - 32; int top = itemView.getTop() + (itemView.getHeight() - iconHeight) / 2; int right = itemView.getRight() - 32; int bottom = top + iconHeight; icon.setBounds(left, top, right, bottom); icon.draw(c); } super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); } } ``` 在Activity或Fragment中,可以将SwipeController作为参数传递给ItemTouchHelper,然后调用ItemTouchHelper.attachToRecyclerView方法来绑定RecyclerView。 ```java RecyclerView recyclerView = findViewById(R.id.recycler_view); Adapter adapter = new Adapter(data); recyclerView.setAdapter(adapter); SwipeController swipeController = new SwipeController(adapter); ItemTouchHelper itemTouchHelper = new ItemTouchHelper(swipeController); itemTouchHelper.attachToRecyclerView(recyclerView); ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值