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閱讀 228,238評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,631評論 25 708
  • 概述 隨著2014年Google IO的召開,Android L Preview版隨之發布,對于開發著來說,帶來了...
    小鄧子閱讀 35,778評論 33 225
  • 這篇文章分三個部分,簡單跟大家講一下 RecyclerView 的常用方法與奇葩用法;工作原理與ListView比...
    LucasAdam閱讀 4,406評論 0 27
  • 前幾天回老家。 我踏進家門口的那一霎那,就看見奶奶沖著我笑,開心的不得了。 我們村里平常日子基本沒什么人,很冷清,...
    許沐笙閱讀 848評論 0 2
  • 團子很饞,可以說是我養過的狗里面最饞的一個,原來是朱莉比較饞,愛吃水果和人的飯,只要看見我們手里的蘋果、雞蛋啥的,...
    午后窗臺的貓閱讀 215評論 0 0