MaterialDesign--(2)让我欢喜让我忧的 RecyclerView

RecyclerView 是伴随着 android5.x 出来的控件,第一次提出应该是在14年的 Google I/O 大会(猜测,懒得查,反正我不 care 它是什么时候出来的),到现在17年 Google I/O 大会结束正好三年,相信大家都早已经把 RecyclerView 使用到项目当中了。

我们都知道,RecyclerView 的出现,是为了取代 ListView、GridView 而出现的。记得有次面试的时候,面试官问我为什么要使用 RecyclerView,你 RecyclerView 能实现的列表,我 ListView 同样可以实现,我当时是这样回答的:整体上看RecyclerView架构,提供了一种插拔式的体验,高度的解耦,异常的灵活,这段话我随便在网上复制的,大意差不多。好了,扯远了。

  • 为什么说 RecyclerView 让我欢喜让我忧?
    欢喜因为用了 RecyclerView 之后,感觉就再也不想去写 ListView 了;忧则是因为尽管用了几年 RecyclerView,但直到现在感觉 RecyclerView 还是玩不溜,同感玩不溜的同学请握个抓。

  • 为什么说 RecyclerView 高度解耦
    我们来看看 RecyclerView 的几个大家熟悉的方法:

    mRecyclerView.setLayoutManager(layout);
    //设置条目布局规则
    mRecyclerView.setItemAnimator();
    //设置条目动画
    mRecyclerView.addItemDecoration();
    //自定义条目装饰
    mItemTouchHelper.attachToRecyclerView(mRecyclerView);
    //定制条目触摸
    现在我们来回顾一下,对应的这几个规则,我们的ListView 是怎么实现的,

  • layoutmanger:ListView 并不支持这个功能

  • ItemAnimation:在 Adapter的 getView()方法里面给创造出来的 View 加动画

  • ItemDecoration:在 Adapter 的 getView()方法里面自行处理

  • ItemTouchHelp: 在 Adapyer 的 getView()方法里面自行给 View 设置 Touch 事件处理。

对比一下,瞬间感觉 ListView 弱爆了。。。

说了这么多优点,说说缺点,既然 RecyclerView 给我们提供了这么多扩展,可以高度定制,但是上手难度同样增加了不止一个等级。还有条目点击事件,简直丧心病狂,竟然接口都没提供。

好了,我们要开始写代码了。

一、RecyclerView 的基本使用

先实现一个简单的列表:

RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycle_view);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    recyclerView.setAdapter(new RecyclerView.Adapter() {
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            //创建一个 ViewHolder 并且返回
            return null;
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            //在这里给 ViewHolder 绑定数据
        }

        @Override
        public int getItemCount() {
         //控制RecyclerView的条目数
            return 0;
        }
    });

这样我们就使用RecyclerView实现了一个简单的列表,由于代码比较简单,我直接一笔带过了。这里对比我们的 Listview,就多了一行代码recyclerView.setLayoutManager(new LinearLayoutManager(this));,至于这行代码具体有什么作用呢,我们后面再说。

二、封装RecyclerView 的 Adapter

Adapter是 RecycleView 中最重要的一个类,虽然没有特别复杂难理解的代码,但是 RecycleView 的刷新条目、多条目、点击事件都需要在里面处理。而且每一个 RecycleView 的 Adapter 几乎都需要带读写,所以这是一个高频率、代码量稍多的一个类,因此我们在使用的过程中一般会对 Adapter 进行一下封装。

  • 数据处理用泛型规范输入数据

我们在实际项目开发当中,会有很多地方都用到 RecycleView(太长了,下面我用 rv 简称吧),而且数据的结构各有不同,因此,在 BaseAdapter 里面,我们需要用一个泛型 T 去规范数据结构的类型,避免误操作。

  • 默认实现条目的增删方法和getItemCount();

这个比较简单,等下直接看代码

  • 配合 ButtonKnife,简化 ViewHolder 里面的 findViewbyid 操作

这个也简单,抽取了一个 BaseViewHolder,在构造方法里面绑定的。

  • 好像也没什么好写的,我直接贴我项目中封装的 BaseAdapter 的代码吧
public abstract class BaseAbstractAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
  protected final String TAG = getClass().getSimpleName();
  protected final Context mContext;
  protected final LayoutInflater mLayoutInflater;

  protected List<T> mDataList = new ArrayList<>();

  public BaseAbstractAdapter(Context context) {
      this.mContext = context;
      this.mLayoutInflater = LayoutInflater.from(mContext);
  }

  public Context getContext() {
      return mContext;
  }
  
  public List<T> getDataList() {
       return mDataList;
  }
  
   public T getItemData(int position) {
      return (position >= 0 && position < mDataList.size()) ? mDataList.get(position) : null;
  }
  
   @Override
  public int getItemCount() {
      return mDataList == null ? 0 : mDataList.size();
  }

  /**
   * 移除某一条记录
   *
   * @param position 移除数据的position
   */
  public void removeItem(int position) {
      if (position >= 0 && position < mDataList.size()) {
          mDataList.remove(position);
          notifyItemRemoved(position);
      }
  }

  /**
  * 添加一条记录
   *
  * @param data     需要加入的数据结构
  * @param position 插入位置
  */
  public void addItem(T data, int position) {
      if (position >= 0 && position <= mDataList.size()) {
          mDataList.add(position, data);
          notifyItemInserted(position);
      }
  }

  /**
   * 添加一条记录
   *
   * @param data 需要加入的数据结构
   */
  public void addItem(T data) {
      addItem(data, mDataList.size());
  }

  /**
   * 移除所有记录
   */
  public void clearItems() {
      int size = mDataList.size();
      if (size > 0) {
          mDataList.clear();
          notifyItemRangeRemoved(0, size);
      }
  }

  /**
   * 批量添加记录
   *
   * @param data     需要加入的数据结构
   * @param position 插入位置
   */
  public void addItems(List<T> data, int position) {
      if (position >= 0 && position <= mDataList.size() && data != null && data.size() > 0) {
          mDataList.addAll(position, data);
          notifyItemRangeChanged(position, data.size());
      }
  }

  /**
   * 批量添加记录
   *
   * @param data 需要加入的数据结构
   */
  public void addItems(List<T> data) {
      addItems(data, mDataList.size());
  }

  @Override
  public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
      if (holder instanceof BaseViewHolder) {
          ((BaseViewHolder) holder).bindViewData(getItemData(position));
      }
  }

  }    
  //不是内部类哦
  public abstract class BaseViewHolder<T> extends RecyclerView.ViewHolder {

  public BaseViewHolder(View itemView) {
      super(itemView);
      ButterKnife.bind(this, itemView);
  }

  public abstract void bindViewData(T data);
  }

好了,代码贴完了,应该没有什么难懂的代码吧,这一套封装基本上可以满足98%以上的单Item 列表了,至于点击事件,并不是所有的列别都需要,根据实际需要自己定义接口回调就行了。

到这里可能有的同学会问,那多**** Item ****的**** rv ****怎么办,在实际开发中,多**** Item ****的**** rv ****情景也不算少,特别是聊天列表,动辄八九种**** Item****。别急,我们慢慢来****~****

rv 的方法中有个抽象方法onCreateViewHolder(ViewGroup parent, int viewType),这个方法是用来创建 ViewHolder 的,ViewHolder 我们可以把它理解成RecycleView 一个 ItemView 的包装类,也就是说一个 ViewHolder 就是一个条目,如果我们需要多条目,那么直接在这里返回不同的条目就行了,方法参数里面正好有个 viewType可以用来控制条目类型。

那么问题来了,这个 viewType值是从哪里来的呢,想知道这个,那就只能去看源码了,我们通过产看 RecyclerView.Adapter 的源码发现,onCreateViewHolder方法是在createViewHolder里面调用

public final VH createViewHolder(ViewGroup parent, int viewType) {
        TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
        final VH holder = onCreateViewHolder(parent, viewType);
        holder.mItemViewType = viewType;
        TraceCompat.endSection();
        return holder;
    }

看到这里,我们只能继续追createViewHolder的调用。然后通过全局搜索,在getViewForPosition方法里面找到了viewType这个参数的来源,里面有一行代码 final int type = mAdapter.getItemViewType(offsetPosition);于是再继续追getItemViewType。

 public int getItemViewType(int position) {
        return 0;
    }

好了,追到这里我也不再赘述了,本来大家都知道重写getItemViewType方法就行了。
回到正题,怎么封装多 ItemAdapter。多 Item 用到的场景一般都是需要给 RV 添加一个头或者添加一个尾,因此,考虑到通用性,我就只做了三种类型条目的扩展,下面直接贴代码:

 public abstract class BaseAbstractMultipleItemAdapter<T> extends BaseAbstractAdapter<T> {
 private static final int ITEM_TYPE_HEADER = 1;
 private static final int ITEM_TYPE_BOTTOM = 2;
 private static final int ITEM_TYPE_CONTENT = 3;

 @IntDef({ITEM_TYPE_HEADER, ITEM_TYPE_BOTTOM})
 @interface ItemType {

 }

 protected int mHeaderCount;//头部View个数
 protected int mBottomCount;//底部View个数

 public BaseAbstractMultipleItemAdapter(Context context) {
     super(context);
 }

 public void setHeaderCount(int headerCount) {
     this.mHeaderCount = headerCount;
 }

 public void setBottomCount(int bottomCount) {
     this.mBottomCount = bottomCount;
 }

 public int getHeaderCount() {
     return mHeaderCount;
 }

 public int getBottomCount() {
     return mBottomCount;
 }

 public boolean isHeaderView(int position) {
     return mHeaderCount != 0 && position < mHeaderCount;
 }

 public boolean isBottomView(int position) {
     return mBottomCount != 0 && position >= (mHeaderCount + super.getItemCount());
 }

 @Override
 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
     if (viewType == ITEM_TYPE_HEADER) {
         return onCreateHeaderView(parent);
     } else if (viewType == ITEM_TYPE_BOTTOM) {
         return onCreateBottomView(parent);
     } else {
         return onCreateContentView(parent, viewType);
     }
 }

 @Override
 public int getItemViewType(int position) {
     if (isHeaderView(position)) {//头部View
         return ITEM_TYPE_HEADER;
     } else if (isBottomView(position)) {//底部View
         return ITEM_TYPE_BOTTOM;
     } else {
         return getContentViewType(position);
     }
 }

 @Override
 public int getItemCount() {
     return mHeaderCount + super.getItemCount() + mBottomCount;
 }

 @Override
 public T getItemData(int position) {
     int index = position - mHeaderCount;
     if (index >= super.getItemCount()) {
         return null;
     }
     return super.getItemData(index);
 }

 /**
  * 移除某一条记录
  *
  * @param position 移除数据的position 如果有Header需要减去Header数量
  */
 public void removeItem(int position) {
     if (position < mDataList.size()) {
         mDataList.remove(position);
         notifyItemRemoved(mHeaderCount + position);
     }
 }

 /**
  * 添加一条记录
  *
  * @param data     需要加入的数据结构
  * @param position 插入数据的位置 如果有Header需要减去Header数量
  */
 public void addItem(T data, int position) {
     if (position <= mDataList.size()) {
         mDataList.add(position, data);
         notifyItemInserted(mHeaderCount + position);
     }
 }


 /**
  * 移除所有记录
  */
 public void clearItems() {
     int size = mDataList.size();
     if (size > 0) {
         mDataList.clear();
         notifyItemRangeRemoved(mHeaderCount, size);
     }
 }


 /**
  * 批量添加记录
  * @param data     需要加入的数据结构
  * @param position 插入数据的位置 如果有Header需要减去Header数量
  */
 public void addItems(List<T> data, int position) {
     if (position <= mDataList.size() && data != null && data.size() > 0) {
         mDataList.addAll(position, data);
         notifyItemRangeChanged(mHeaderCount + position, data.size());
     }
 }

 public int getContentViewType(int position) {
     return ITEM_TYPE_CONTENT;
 }

 public RecyclerView.ViewHolder onCreateHeaderView(ViewGroup parent) {//创建头部View
     return null;
 }

 public abstract RecyclerView.ViewHolder onCreateContentView(ViewGroup parent, int viewType);//创建中间内容View

 public abstract RecyclerView.ViewHolder onCreateBottomView(ViewGroup parent);//创建底部View

}

代码都有注释,应该没有什么看不懂多看几遍不能理解的通过mHeaderCount和mBottomCount分别控制头尾条目数,然后需要什么条目就重写对应的 CreateViewHolder 方法即可,如果 head 或者 bottom 需要绑定数据就在onBindViewHolder里面根据holder 和 position自行绑定。

三、ItemDecoration

Decoration:n.装饰品;装饰,装潢;装饰图案,装饰风格;奖章
顾名思义,条目装饰。

我们在写开发中经常会遇到这个的需求,列表的条目和条目之间需要添加一个间隔线,不知道你们是怎么解决的,反正我之前是直接在条目布局的底部直接写了一个分割线进去。这样写当然也可以,虽然最后一个条目不需要分割线可以通过代码手动隐藏掉,但是真的很 low有木有,做为一个自命不凡的 Coder,怎么写出如此高耦合重复的代码。

ItemDecoration 就可以完美的帮我们解决分割线的问题,当然ItemDecoration的功能可不仅仅如此,一口吃不成胖子,我们一步一步来

使用:mRecycleView.addItemDecoration(new RecyclerView.ItemDecoration() {});然而这只是一个抽象类。。。

首先我们点进源码,看这个类的注释

 /**
 * An ItemDecoration allows the application to add a special drawing and layout offset
 * to specific item views from the adapter's data set. This can be useful for drawing dividers
 * between items, highlights, visual grouping boundaries and more.
 *
 * <p>All ItemDecorations are drawn in the order they were added, before the item
 * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
 * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
 * RecyclerView.State)}.</p>
 */

我英语不怎么好,就不一句一句的翻译了,大意就是:
一个ItemDecoration允许添加一个特殊的图形和布局偏移。比如说涌入绘制项目之间的分割、突出显示、视觉分组边界等等。
所有的 ItemDecoration 的绘制顺序和条目添加顺序一致,后面这句话我翻译不通顺🤦‍♀️。

查看了一下ItemDecoration类的继承关系,发现Google 给我们默认实现的子类就只有一个ItemTouchHelper,这是一个比较特殊的子类,我们在后面会讲。那么既然这样,我们就只能手撸。

手撸之前我们先看一下ItemDecoration类的结构,在这里提取出三个比较关键得方法

 public void onDraw(Canvas c, RecyclerView parent, State state)
 public void onDrawOver(Canvas c, RecyclerView parent, State state)
 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)

源码上面三个方法都有方法说明,但是英语渣的我就不误导大家的英语了,我直接讲方法吧。

  • getItemOffsets()
    顾名思义,获取条目偏移,可以实现类似padding的效果,我这里偷了一张图便于大家理解:


    图片来源于网络、不知道哪位好汉的原创

  • onDraw()
    绘制背景,就是绘制的东西会在条目的下层

  • onDrawOver()
    绘制覆盖物,就是绘制的东西会覆盖在条目上

可能还有点懵逼,但是知道了这三个方法,我们就可以动手给 RecycleView 设置分割线了,下面是一个设置1px 分割线的代码

 public class DividerDecoration extends RecyclerView.ItemDecoration {

private int dividerHeight;
private Paint dividerPaint;

 public SimpleDividerDecoration(Context context) {
     dividerPaint = new Paint();
     dividerPaint.setColor(Color.RED);
     dividerHeight = 1     
 }


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

 @Override
 public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
     int childCount = parent.getChildCount();
     int left = parent.getPaddingLeft();
     int right = parent.getWidth() - parent.getPaddingRight();
     for (int i = 0; i < childCount - 1; i++) {
         View view = parent.getChildAt(i);
         float top = view.getBottom();
         float bottom = view.getBottom() + dividerHeight;
         c.drawRect(left, top, right, bottom, dividerPaint);
     }
   }
 }

注意:这里有个坑getItemOffsets()里面最好不要调用 super 方法,因为里面有一个默认实现outRect.set(0, 0, 0, 0);super 方法最先调用还好,如果在最后调用,我们的outRect参数就被重置了。

四、ItemAnimator

顾名思义,条目动画。本来不想写的,RecyclerView 有默认的实现动画,而且列表中根本用不到酷炫的条目动画,无奈被我早起规划的时候,加入了 RecyclerView 的知识点里面,在这里简单讲一下吧。
使用方法如下:

 public void setItemAnimator(ItemAnimator animator) {
    if (mItemAnimator != null) {
        mItemAnimator.endAnimations();
        mItemAnimator.setListener(null);
    }
    mItemAnimator = animator;
    if (mItemAnimator != null) {
        mItemAnimator.setListener(mItemAnimatorListener);
    }
}

从这个方法里面,我们可以 get 到两个重要信息:

  • 自定义条目动画必须继承ItemAnimator。
  • RecyclerView 有默认的实现动画 DefaultAnimation,并且在定义变量的时候就赋值给mItemAnimator。

好了,那么接下来我们就通过学习DefaultAnimation的实现来自定义 ItemAnimation。

DefaultAnimation 继承自SimpleItemAnimator,通过阅读 SimpleItemAnimator 的注释信息,我们知道它是RecyclerView.ItemAnimator的直接子类,并且添加了ItemHolderInfo(一个简单的数据结构,保存了 Item 的边界信息,用于计算项目动画)来辅助条目动画的执行。因此我们如果要自定义 ItemAnimation 最好继承自SimpleItemAnimator。

好了,到这里,我们算是知道了如何自定义 ItemAnimation,接下来我们只需要顺藤摸瓜就可以了。

要顺藤摸瓜,得先找到藤,我们去看看ItemAnimation的抽象方法,自定义 ItemAnimation 一共有八个相关的方法需要我们去手动实现

 //Item移除回调
 @Override
 public boolean animateRemove(RecyclerView.ViewHolder holder) {
     return false;
 }

 //Item添加回调
 @Override
 public boolean animateAdd(RecyclerView.ViewHolder holder) {
     return false;
 }

 //用于控制添加,移动更新时,其它Item的动画执行
 @Override
 public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
     return false;
 }

 //Item更新回调
 @Override
 public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
     return false;
 }

 //真正控制执行动画的地方
 @Override
 public void runPendingAnimations() {

 }
 //停止某个Item的动画
 @Override
 public void endAnimation(RecyclerView.ViewHolder item) {

 }

 //停止所有动画
 @Override
 public void endAnimations() {

 }

 @Override
 public boolean isRunning() {
     return false;
 }

没个方法的作用我都在上面写了注释,相信看懂应该不难,接下来我们再根据这根藤回到 DefaultItemAnimation。

  • animateAdd开始吧,方法很简单,就三行代码。一是清楚和删除 Item 里面所有的动画相关代码,二是把条目初始化为透明状态(可对比默认执行动画),三是把条目添加到等待运行动画列表里面。
  @Override
   public boolean animateAdd(final ViewHolder holder) {
     resetAnimation(holder);
     ViewCompat.setAlpha(holder.itemView, 0);
     mPendingAdditions.add(holder);
     return true;
   }
  • animateRemove,和animateAdd一样,就少了一行初始化条目的代码。
  @Override
   public boolean animateRemove(final ViewHolder holder) {
     resetAnimation(holder);
     mPendingRemovals.add(holder);
     return true;
 }
  • 再看animateMove()方法,忘记这个方法作用的同学请再回头看看。这个方法里面有几个参数,分别是要移动的条目,起始 xy 轴的位置。里面的操作也很简单,移动Item,然后保存 Item 的移动信息。
  @Override
     public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
         int toX, int toY) {
     final View view = holder.itemView;
     fromX += ViewCompat.getTranslationX(holder.itemView);
     fromY += ViewCompat.getTranslationY(holder.itemView);
     resetAnimation(holder);
     int deltaX = toX - fromX;
     int deltaY = toY - fromY;
     if (deltaX == 0 && deltaY == 0) {
         dispatchMoveFinished(holder);
         return false;
     }
     if (deltaX != 0) {
         ViewCompat.setTranslationX(view, -deltaX);
     }
     if (deltaY != 0) {
         ViewCompat.setTranslationY(view, -deltaY);
     }
     mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
     return true;
 }
  • 接下来看animateChange(),我们看到,如果是同一个 ViewHolder 则直接调用 animateMove()方法,否则在内部多记录了一个 alpha 的值
  @Override
   public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
         int fromX, int fromY, int toX, int toY) {
     if (oldHolder == newHolder) {
         // Don't know how to run change animations when the same view holder is re-used.
         // run a move animation to handle position changes.
         return animateMove(oldHolder, fromX, fromY, toX, toY);
     }
     final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
     final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
     final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
     resetAnimation(oldHolder);
     int deltaX = (int) (toX - fromX - prevTranslationX);
     int deltaY = (int) (toY - fromY - prevTranslationY);
     // recover prev translation state after ending animation
     ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
     ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
     ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
     if (newHolder != null) {
         // carry over translation values
         resetAnimation(newHolder);
         ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
         ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
         ViewCompat.setAlpha(newHolder.itemView, 0);
     }
     mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
     return true;
 }
  • endAnimation()方法和endAnimations()方法一样,就是循环把待处理的动画信息全部删掉,然后调用 cancelAll()停止正在运行的动画。

  • isRunning,通过判断动画队列,看是否有动画待执行动画或者正在执行的动画

  • runPendingAnimations(),真正执行动画的地方,判断待执行的动画队列里面是否有需要执行的动画,如果有,就顺序执行,如果没有就退出。

  • animateAddImpl()、animateChangeImpl()、animateMoveImpl、animateRemoveImpl()这几个方法分别是几种类型动画的具体实现。

好累啊😫,终于把它分析完了,接下来我们一鼓作气,动手撸一个。但是我项目中好像没有现成的,然后写动画这种需要创意的事情我心累,而且关键是实际开发中几乎也不怎么用得到。但是有一部分同学可能会对动画比较感兴趣,于是机智的我去 github 上找了一个RecyclerView 的 Item 动画库,大家可以对着我的分析去看看别人的实现,贴上传送门:https://github.com/wasabeef/recyclerview-animators

五、ItemTouchHelper

ItemTouchHelper 条目触摸助手,顾名思义,这个类就是用来帮助我们处理 RV 条目触摸事件的,如常见的滑动删除,长按拖拽。效果图如下:

ItemTouchHelper.gif
ItemTouchHelper2.gif

没有啥特别的,我直接贴代码吧:

 ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
     @Override
     public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
         int from = viewHolder.getAdapterPosition();
         int to = target.getAdapterPosition();
         if (from < to) {
             for (int i = from; i < to; i++)
                 Collections.swap(mNewTopsAdapter.getDataList(), i, i + 1);
         } else {
             for (int i = from; i > to; i--)
                 Collections.swap(mNewTopsAdapter.getDataList(), i, i - 1);
         }
         mNewTopsAdapter.notifyItemMoved(from, to);
         return false;
     }

     @Override
     public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
         mNewTopsAdapter.removeItem(viewHolder.getAdapterPosition());

     }
 });
itemTouchHelper.attachToRecyclerView(mRecycleView);

实现起来很简单,new一个 ItemTouchHelper, 然后调用ItemTouchHelper的 attachToRecyclerView() 依附给 RV 就行了。关键点在 ItemTouchHelper 的构造方法里面必传的参数ItemTouchHelper.Callback()。由于这里需求比较简单,我直接用了已经做过一次封装的SimpleCallback。

SimpleCallback:继承自ItemTouchHelper.Callback,对父类进行了包装,只暴露出两个简单的方法供开发者去实现。

构造方法:

  • SimpleCallback(int dragDirs, int swipeDirs)
  • dragDirs:条目的拖动方向,可配参数有ItemTouchHelper.UP、ItemTouchHelper.DOWN、ItemTouchHelper.LEFT、ItemTouchHelper.RIGHT,如果要同时配置多个方向,用运算符号|连接
  • swipeDirs:条目滑动方向,参数类型同上

我们在 new SimpleCallback()的时候把拖拽的参数配置好,然后再实现如下两个抽象方法

  • public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
  • public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)

其中 onMove方法会在 Item 拖动的时候不断调用,此时我们需要在条目拖动的时候调用 adapter 的notifyItemMoved()方法刷新条目位置,具体代码如下:

 int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
if (from < to) {
    for (int i = from; i < to; i++)
   Collections.swap(mNewTopsAdapter.getDataList(), i, i + 1);
} else {
    for (int i = from; i > to; i--)
        Collections.swap(mNewTopsAdapter.getDataList(), i, i - 1);
}
mNewTopsAdapter.notifyItemMoved(from, to);
return true;
首先获取当前条目和目标条目position,这里需要注意getAdapterPosition()和getLayoutPosition(),前者是在adapter 调用界面刷新的时候就给 position 赋值了,而后者是在界面刷新结束之后才能获取到正确的赋值。我们都知道,RV 的界面刷新是异步的,大概会有一个16毫秒左右的延时,因此使用getLayoutPosition()获取 position 可能会出错哦。

onSwiped方法则是用来控制条目滑动删除之后的逻辑处理。其中direction参数是滑动的方向。比如说滑动删除:我们直接在方法体里面调用 adapter 的 notifyItemRemoved()方法即可。

好了,ItemTouchHelper 的基本用法就这些,基本也能满足开发过程中的大部分需求了,如果还有更高的需求,那么继续跟我去肛一波ItemTouchHelper.Callback的源码。

****ItemTouchHelper.Callback****
抽象方法有三个:

  • public int getMovementFlags(RecyclerView, RecyclerView.ViewHolder)
  • public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
  • public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)

其中第二个第三个方法,我们在SimpleCallback已经解释过一次了,这里不在赘述。我们来讲一下第一个方法

  • public abstract int getMovementFlags(RecyclerView recyclerView,ViewHolder viewHolder);

方法说明上,我用我三级英语水平加上翻译软件,大概可以读出这个方法是要返回一个控制 item 移动方向的混合标志,混合标志怎么生成,可以使用方法makeMovementFlags();好,那么实现getMovementFlags的方法体大概就是 return getMovementFlags(dragFlags,swipeFlags);
而 getMovementFlags 要求我们传两个参数,这两个参数怎么传呢,我们继续去追getMovementFlags();

 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);
    }

方法说明上,我们可以知道这个方法是用来创建移动 flag 的,说白了就是用来控制 Item 的移动/滑动方向,方法中两个参数分别是拖动 flag 和滑动 flag。看到这里,我们来回想一下SimpleCallback的构造方法,是不是也要传这两个参数,而SimpleCallback不需要实现 getMovementFlags()方法,是不是因为已经帮我们实现了,通过查看源码验证了我们的猜想。

然后就是一些公共方法,可重写定制的:

 //是否可以把拖动的ViewHolder拖动到目标ViewHolder之上
 @Override
 public boolean canDropOver(RecyclerView recyclerView,RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
return true;
 }

 //获取拖动
 @Override
 public RecyclerView.ViewHolder chooseDropTarget(RecyclerView.ViewHolder selected, List<RecyclerView.ViewHolder> dropTargets, int curX, int curY) {
return dropTargets.get(0);
 }

 //调用时与元素的用户交互已经结束,也就是它也完成了它的动画时候
 @Override
 public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
 }

 @Override
 public int convertToAbsoluteDirection(int flags, int layoutDirection) {
return super.convertToAbsoluteDirection(flags, layoutDirection);
 }

 //设置手指离开后ViewHolder的动画时间
 @Override
 public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
return super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy);
 }

 @Override
 public int getBoundingBoxMargin() {
return super.getBoundingBoxMargin();
 }

 //返回值作为用户视为拖动的距离
 @Override
 public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) {
return super.getMoveThreshold(viewHolder);
 }

 //返回值滑动消失的距离,滑动小于这个值不消失,大于消失
 @Override
 public float getSwipeEscapeVelocity(float defaultValue) {
return super.getSwipeEscapeVelocity(defaultValue);
 }

 //返回值滑动消失的距离, 这里是相对于RecycleView的宽度,0.5f表示为RecycleView的宽度的一半,取值为0~1f之间
 @Override
 public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
return super.getSwipeThreshold(viewHolder);
 }

 //返回值作为滑动的流程程度,越小越难滑动,越大越好滑动
 @Override
 public float getSwipeVelocityThreshold(float defaultValue) {
return 1f;
 }

 //当用户拖动一个视图出界的ItemTouchHelper调用
 @Override
 public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll) {
return super.interpolateOutOfBoundsScroll(recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
 }

 //返回值决定是否有滑动操作
 @Override
 public boolean isItemViewSwipeEnabled() {
return super.isItemViewSwipeEnabled();
 }

 //返回值决定是否有拖动操作
 @Override
 public boolean isLongPressDragEnabled() {
return super.isLongPressDragEnabled();
 }

 //自定义拖动与滑动交互
 @Override
 public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
 }

 //自定义拖动与滑动交互
 @Override
 public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
 }

 //当onMove return ture的时候调用
 @Override
 public void onMoved(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int fromPos, RecyclerView.ViewHolder target, int toPos, int x, int y) {
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y);
 }

 //当拖动或者滑动的ViewHolder改变时调用
 @Override
 public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
 }

六、LayoutManger

LayoutManger:布局管理
LayoutManger是 RecyclerView 用来管理子 view 布局的一个组件(另一个组件是 Recycler,负责回收视图),它主要负责三个事情:

1.布局子视图
2.在滚动的过程中根据子视图在布局中所处的位置,决定何时添加子视图和回收视图
3.滚动子视图

其中只有滚动子视图才需要对子视图回收或添加,而添加子视图则必然伴随着所添加对象的布局处理,在滚动过程中,添加一次子视图只会影响到被添加对象,原有子视图的相对位置不会变化。

LayoutManger 是 RecyclerView 的一个抽象内部类,一般我们使用它都是使用它的子类:

  • LinearLayoutManager
  • GridLayoutManager
  • StaggeredGridLayoutManager

这三个类的用法我就不过多的赘述了,相信大家都用过,一般情况下,这三个 LayoutManger 也能够满足大家99%的需求了。自定义LayoutManger 是一件比较有难度的工程,而且使用场景很少(反正我是没碰到过这样的需求)。但是网上有很多炫酷的自定义 LayoutManger 效果,最经典的当属防探探的卡片式布局,在网上也看过很多自定义 LayoutManger 的文章,但现在还是半吊子。

感兴趣的同学可以看看这个库,里面有 bolg 链接。当然,需要的时候再去看也行。传送门:https://github.com/mcxtzhang/ZLayoutManager

好了,RecyclerView 到这里就讲完了,可能有些地方深度不够,但是基本能满足大部分的需求了,如果有问题可以留言提问,后者直接私信我。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,577评论 25 707
  • 概述 随着2014年Google IO的召开,Android L Preview版随之发布,对于开发着来说,带来了...
    小鄧子阅读 35,587评论 33 225
  • 这篇文章分三个部分,简单跟大家讲一下 RecyclerView 的常用方法与奇葩用法;工作原理与ListView比...
    LucasAdam阅读 4,320评论 0 27
  • 前几天回老家。 我踏进家门口的那一霎那,就看见奶奶冲着我笑,开心的不得了。 我们村里平常日子基本没什么人,很冷清,...
    许沐笙阅读 796评论 0 2
  • 团子很馋,可以说是我养过的狗里面最馋的一个,原来是朱莉比较馋,爱吃水果和人的饭,只要看见我们手里的苹果、鸡蛋啥的,...
    午后窗台的猫阅读 198评论 0 0