How to implement RecyclerView ItemTouchHelper、ItemDecoration and GridLayoutManager.SpanSizeLookup

轉載請註明出處:https://medium.com/@devwilly,違者必究

2014 Google I/O 發表了Android L同時也在系統中推出了全新的元件RecyclerView,官方宣稱此元件比ListView更好用(更彈性)。

沿用TabLayout-Design Library範例,將針對RecyclerView常用的類別進行詳細介紹,以下會將重點放在ItemTouchHelperItemDecorationGridLayoutManager.SpanSizeLookup


How to use GridLayoutManager.SpanSizeLookup ?

當建立RecyclerView時需要提供裝載child view的容器(layout)且容器能依據child view的種類提供不同的排列方式。

void setLayoutManager (RecyclerView.LayoutManager layout)

RecyclerView 項目顯示方式不局限於垂直一個個排列,也能透過LayoutManager 來改變它,若想呈現GridView感覺就必需使用GridLayoutManager ,使用方法如下:

rv.setLayoutManager(new GridLayoutManager(getContext(), 2));

GridLayoutManager 第二個參數為spanCount,其定義為一個畫面可被分割成幾等份

圖片資料均屬原公司所有

接著若想針對不同的項目類型進行容器大小的調整該如何實作呢?

以GridLayoutManager為例,藉由覆寫getSpanSize()來決定各類別項目可使用哪種大小的容器

getSpanSize():每個項目佔據多少位置
[WeekMovieSpanSizeLookup.class]
@Override
public int getSpanSize(int position) {
if (mAdapter.getItemCount() == 0) {
return 2;
}

int viewType = mAdapter.getItemViewType(position);

if (viewType == R.layout.vh_section_header) {
return 2;
} else {
return 1;
}
}
spanCount 及 spanSize 比例圖

What’s 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.

簡單來說它是RecyclerView.ItemDecoration的子類別,主要用於處理RecyclerView上的拖移滑動等所有事情。

此外我們需要建立一個ItemTouchHelper.Callback 來監聽「拖動」與「滑動」事件,且能得知被選中的畫面(View)的狀態並針對過場動畫進行改寫。


How to use ItemTouchHelper ?

開始實作前我們必須先了解以下幾個基本概念:

  • 如何決定”Drag”“Swipe”的方向呢?
  • 如何在onMove()onSwiped() 被trigger的過程中更新RecyclerView列表中的資料?

接下來將上述問題拆成幾個區塊來做說明,若有不懂的地方歡迎提問!!

Q1:如何決定「Drag」 或 「Swipe」的方向呢?

A1:必需Override ItemTouchHelper.Callback 中 getMovementFlags() 並將結果透過makeMovementFlags()回傳相對的flag值。

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

從上述程式碼中可得知4個關鍵的參數(UP、DOWN、START、END),它們分別代表:

ItemTouchHelper.UP: 向上拖動(由下往上)
ItemTouchHelper.DOWN:向下拖動(由上往下)
ItemTouchHelper.RIGHT:向右拖動(由左往右)
ItemTouchHelper.LEFT:向左拖動(由右往左)
ItemTouchHelper.START:向右滑動(由右往左)
ItemTouchHelper.END:向左滑動(由左往右)
拖移與滑動未更新資料示意圖

Q2:如何在 onMove() 和 onSwiped() 被trigger的過程中更新RecyclerView列表中的資料?

A2:

  • 建立一個onMove() 和 onSwiped() 被trigger時通知RecyclerView 資料排序已異動的Interface
void onItemMove(int fromPosition, int toPosition);

void onItemSwipe(RecyclerView.ViewHolder viewHolder, int direction);
  • 當Item被拖移(Move)時通知RecyclerView進行資料更新
[SimpleItemTouchHelperCallback.class]
public boolean onMove(RecyclerView recyclerView,  
RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {

int fromPosition = viewHolder.getAdapterPosition();
int toPosition = target.getAdapterPosition();

if (toPosition > 0) {
((MovieListAdapter)recyclerView.getAdapter())
.onItemMove(fromPosition, toPosition);
}

return true;
}
[MovieListAdapter.class]
@Override
public void onItemMove(int fromPosition, int toPosition) {
Collections.swap(mDataList, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
}

以下展示Item移動過程中Header viewHolder不被影響實作:

Header ViewHolder 不受item move影響

  • 當Item被刪除時通知RecyclerView進行資料更新,若不想刪除資料可將它重新加入列表中

How to use 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.

顧名思義就是拿來裝飾RecyclerView Item用的,最常使用的方法是getItemOffsets()此方法用於調整每個Item間的間距大小。

今日要介紹的是兩個較少使用到的方法:

onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)
onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)
  • onDraw():繪製在Item的下層
  • onDrawOver():繪製在Item的上層

首先我們利用onDraw()在每個Item下方繪製一塊大小一樣的背景顏色,實作的重點是:

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

int itemCount = parent.getChildCount();

for (int i = 0; i < itemCount; i++) {
View child = parent.getChildAt(i);
int itemViewType =
parent.getChildViewHolder(child).getItemViewType();

switch (itemViewType) {
case R.layout.vh_item_week:
int childLeft = child.getLeft();
int childTop = child.getTop();
int childRight = child.getRight();
int childBottom = child.getBottom();

c.drawRect(childLeft, childTop,
childRight, childBottom, mPaint);

break;
}

}
}
ItemDecoration onDraw()

接著利用onDrawOver() 在Item邊界上繪製一條寬度與getItemOffsets()一樣間距的線段,實作重點與onDraw()類似。(可參考

ItemDecoration onDrawOver()