一、簡述
RecyclerView預設就有item動畫,例如在增加或刪除item時,都會有一個條目間位移的動畫,但本文要說的不是這個!!!本文的主角是v7包中的ItemTouchHelper,它跟RecyclerView結合後將會帶來神奇的互動效果。示例如下:
效果還是比較酷炫的吧,上圖中有四步操作:
- 長按item後拖動,與其他item交換位置
- 按住item右面的圖示後拖動,與其他item交換位置
- 左滑item變透明並縮小,超出螢幕後,其他item補上
- 右滑item變透明並縮小,超出螢幕後,其他item補上
下面將一一實現出這些效果
二、初識ItemTouchHelper
1、建立ItemTouchHelper
// 建立ItemTouchHelper,並跟RecyclerView繫結
mItemTouchHelper = new ItemTouchHelper(mCallback);
mItemTouchHelper.attachToRecyclerView(mRv);複製程式碼
上面就是ItemTouchHelper在本例中出場的三行程式碼中的兩行程式碼,但這並不能完成上面的效果,ItemTouchHelper只是一箇中間人,它將ItemTouchHelper.Callback和RecyclerView連線起來,具體效果還需要由Callback實現。
2、自定義ItemTouchHelper.Callback
1)建立一個自己的Callback
繼承ItemTouchHelper.Callback後必須實現如下三個方法。
public class MyItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
return 0;
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder targetViewHolder) {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}
}複製程式碼
2)重寫getMovementFlags()
getMovementFlags()是用來判斷RecyclerView上的哪些方向操作交由ItemTouchHelper.Callback控制,詳細介紹如下:
/**
* 獲取動作標識
* 動作標識分:dragFlags和swipeFlags
* dragFlags:列表滾動方向的動作標識(如豎直列表就是上和下,水平列表就是左和右)
* wipeFlags:與列表滾動方向垂直的動作標識(如豎直列表就是左和右,水平列表就是上和下)
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
// 如果你不想上下拖動,可以將 dragFlags = 0
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
// 如果你不想左右滑動,可以將 swipeFlags = 0
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
//最終的動作標識(flags)必須要用makeMovementFlags()方法生成
int flags = makeMovementFlags(dragFlags, swipeFlags);
return flags;
}複製程式碼
上面我讓item的上下左右都交由ItemTouchHelper.Callback控制,看下效果:
可以看到左右有效,但上下無效。仔細想想也是,本來就是豎直滾動列表,如果上下都直接交給ItemTouchHelper.Callback控制了,那RecyclerView的列表滾動功能該怎麼辦?所以,要觸發上下拖動的互動效果,肯定有其他開啟的方式。
3)開啟長按item拖動效果
直接重寫ItemTouchHelper.Callback的isLongPressDragEnabled()。
/**
* 是否開啟item長按拖拽功能
*/
@Override
public boolean isLongPressDragEnabled() {
return true;
}複製程式碼
預設返回是false,重寫返回true。現在看下效果如何:
長按後可以拖動了,但是這樣不太方便,接下來實現按住item右圖示進行拖動的效果。
4)開啟按住圖示拖動效果
ItemTouchHelper的startDrag(viewHolder)方法可以手動開啟拖動效果,上面的ItemTouchHelper例項建立在Activity中,而圖示例項在Adapter中,為了降低耦合,這裡先寫一個介面:
public interface ItemDragListener {
void onStartDrags(RecyclerView.ViewHolder viewHolder);
}複製程式碼
介面由Activity實現,在Adapter建立時傳入
public class ItemTouchHelperActivity extends AppCompatActivity implements ItemDragListener {
private ItemTouchHelper mItemTouchHelper;
...
private void setRecyclerView() {
mAdapter = new ItemTouchHelperAdapter(mData, this);
...
}
@Override
public void onStartDrags(RecyclerView.ViewHolder viewHolder) {
mItemTouchHelper.startDrag(viewHolder);
}
}複製程式碼
介面由Adapter的圖示觸控時呼叫
public class ItemTouchHelperAdapter extends RecyclerView.Adapter<ItemTouchHelperAdapter.ItemTouchHelperViewHolder> {
private List<String> mData;
private ItemDragListener mItemDragListener;
public ItemTouchHelperAdapter(List<String> data, ItemDragListener itemDragListener) {
mData = data;
mItemDragListener = itemDragListener;
}
...
@Override
public void onBindViewHolder(final ItemTouchHelperViewHolder viewHolder, int position) {
...
viewHolder.mIvDrag.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
mItemDragListener.onStartDrags(viewHolder);
return false;
}
});
}
}複製程式碼
這樣在讓圖示在觸控時,間接的呼叫了mItemTouchHelper.startDrag(viewHolder),看下效果:
三、深入ItemTouchHelper
上面做到了item的上下拖動和左右滑動效果,但只是當前item的動畫效果罷了,下面繼續完成與其他item互動的效果吧。
1、上下拖動時與其他item進行位置交換
1)原理
其實ItemTouchHelper.Callback本身不具備將兩個item互換位置的功能,但RecyclerView可以,我們可以在item拖動的時候把當前item與另一個item的資料位置交換,再呼叫RecyclerView的notifyItemMoved()方法重新整理佈局,同時,因為RecyclerView自帶item動畫,就可以完成上面的互動效果了。
2)實現
item拖動要在ItemTouchHelper.Callback中監聽,而資料交換處理要在Adapter中進行,為了降低耦合,這裡先寫一個介面:
public interface ItemMoveListener {
boolean onItemMove(int fromPosition, int toPosition);
}複製程式碼
介面由Adapter實現
public class ItemTouchHelperAdapter extends RecyclerView.Adapter<ItemTouchHelperAdapter.ItemTouchHelperViewHolder> implements ItemMoveListener{
...
@Override
public boolean onItemMove(int fromPosition, int toPosition) {
//1、交換資料
Collections.swap(mData, fromPosition, toPosition);
//2、重新整理
notifyItemMoved(fromPosition, toPosition);
return true;
}
}複製程式碼
介面在建立ItemTouchHelper.Callback時傳入,在onMove()中呼叫
public class MyItemTouchHelperCallback extends ItemTouchHelper.Callback {
ItemMoveListener mItemMoveListener;
public MyItemTouchHelperCallback(ItemMoveListener itemMoveListener) {
mItemMoveListener = itemMoveListener;
}
...
/**
* 當item拖拽移動時觸發
*
* @param recyclerView
* @param viewHolder 當前被拖拽的item的viewHolder
* @param targetViewHolder 當前被拖拽的item下方的另一個item的viewHolder
* @return
*/
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder targetViewHolder) {
return mItemMoveListener.onItemMove(viewHolder.getAdapterPosition(), targetViewHolder.getAdapterPosition());
}
}複製程式碼
3)效果:
2、左右滑出螢幕時其他item補上
1)原理
相同的,只要在item滑出螢幕時,將對應的資料刪掉,再呼叫RecyclerView的notifyItemRemoved()方法重新整理佈局即可。
2)實現
item滑動要在ItemTouchHelper.Callback中監聽,而資料刪除處理要在Adapter中進行,所以只需要加上面的介面中增加一個方法:
public interface ItemMoveListener {
...
boolean onItemRemove(int position);
}複製程式碼
介面由Adapter實現
public class ItemTouchHelperAdapter extends RecyclerView.Adapter<ItemTouchHelperAdapter.ItemTouchHelperViewHolder> implements ItemMoveListener{
...
@Override
public boolean onItemRemove(int position) {
//1、刪除資料
mData.remove(position);
//2、重新整理
notifyItemRemoved(position);
return true;
}
}複製程式碼
介面在ItemTouchHelper.Callback在onSwiped()中呼叫
/**
* 當item側滑出去時觸發(豎直列表是側滑,水平列表是豎滑)
*
* @param viewHolder
* @param direction 滑動的方向
*/
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mItemMoveListener.onItemRemove(viewHolder.getAdapterPosition());
}複製程式碼
3)效果:
現在大部分效果已經實現,接下來是細節處理。
3、互動時背景變化
1)原理
在item被拖拽或側滑時修改背景色,當動作結束後將背景色恢復回來,而ItemTouchHelper.Callback中正好有對應這兩個狀態的方法,分別是:onSelectedChanged()、clearView()。
2)實現
/**
* 當item被拖拽或側滑時觸發
*
* @param viewHolder
* @param actionState 當前item的狀態
*/
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
//不管是拖拽或是側滑,背景色都要變化
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE)
viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext().getResources().getColor(android.R.color.darker_gray));
}
/**
* 當item的互動動畫結束時觸發
*
* @param recyclerView
* @param viewHolder
*/
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext().getResources().getColor(android.R.color.white));
}複製程式碼
3)效果
4、左右滑動時item漸變
1)分析及實現
在最開始的圖中,當item被側滑出去的過程中,可以看到item的透明度在漸漸變淺,高度在漸漸變小,其實就是讓item執行了兩種屬性動畫而已,在ItemTouchHelper.Callback中有一個方法可以拿到item被拖拽或滑動時的位移變化,那就是onChildDraw()方法,這樣就很好辦了,看如下程式碼實現:
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
//這句程式碼就是item拖拽和滑動效果的實現,所以這句不能省略
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
//我們只需要在左右滑動時,將透明度和高度的值變小(1 --> 0)
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float value = 1 - Math.abs(dX) / viewHolder.itemView.getWidth();
viewHolder.itemView.setAlpha(value);
viewHolder.itemView.setScaleY(value);
}
}複製程式碼
2)效果(有瑕疵)
可以看到,item在滑動過程中漸漸透明並高度縮小了,但是,我明明是刪除了兩條,怎麼結果列表中多出來兩條空白的資料!這又是為什麼呢?
3)修復
其實上圖中並不是多出了兩條空白資料,它們是正常的資料,只是看不到了,這是因為RecyclerView條目(itemView)覆用導致的,前面在onChildDraw()方法中對itemView設定了透明和縮小,而一個列表中固定只有幾個itemView而已,當那兩個透明縮小的itemView被再次使用時,之前設定的透明度和高度比例已經是0,所以就出現了這種情況,解決方法也很簡單,只要在item被移除後,將itemView的透明度和高度比例設定回來即可,程式碼如下:
/**
* 當item的互動動畫結束時觸發
*
* @param recyclerView
* @param viewHolder
*/
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
...
viewHolder.itemView.setAlpha(1);
viewHolder.itemView.setScaleY(1);
}複製程式碼
4)效果(完美)
到這裡,最開始的所有互動效果都已經實現了。