RecyclerView 之使用 ItemTouchHelper 實現互動動畫

GitLqr發表於2017-05-05

一、簡述

RecyclerView預設就有item動畫,例如在增加或刪除item時,都會有一個條目間位移的動畫,但本文要說的不是這個!!!本文的主角是v7包中的ItemTouchHelper,它跟RecyclerView結合後將會帶來神奇的互動效果。示例如下:

RecyclerView 之使用 ItemTouchHelper 實現互動動畫

效果還是比較酷炫的吧,上圖中有四步操作:

  1. 長按item後拖動,與其他item交換位置
  2. 按住item右面的圖示後拖動,與其他item交換位置
  3. 左滑item變透明並縮小,超出螢幕後,其他item補上
  4. 右滑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控制,看下效果:

RecyclerView 之使用 ItemTouchHelper 實現互動動畫

可以看到左右有效,但上下無效。仔細想想也是,本來就是豎直滾動列表,如果上下都直接交給ItemTouchHelper.Callback控制了,那RecyclerView的列表滾動功能該怎麼辦?所以,要觸發上下拖動的互動效果,肯定有其他開啟的方式。

3)開啟長按item拖動效果

直接重寫ItemTouchHelper.Callback的isLongPressDragEnabled()。

/**
 * 是否開啟item長按拖拽功能
 */
@Override
public boolean isLongPressDragEnabled() {
    return true;
}複製程式碼

預設返回是false,重寫返回true。現在看下效果如何:

RecyclerView 之使用 ItemTouchHelper 實現互動動畫

長按後可以拖動了,但是這樣不太方便,接下來實現按住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),看下效果:

RecyclerView 之使用 ItemTouchHelper 實現互動動畫

三、深入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)效果:

RecyclerView 之使用 ItemTouchHelper 實現互動動畫

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)效果:

RecyclerView 之使用 ItemTouchHelper 實現互動動畫

現在大部分效果已經實現,接下來是細節處理。

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)效果

RecyclerView 之使用 ItemTouchHelper 實現互動動畫

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)效果(有瑕疵)

RecyclerView 之使用 ItemTouchHelper 實現互動動畫

可以看到,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)效果(完美)

RecyclerView 之使用 ItemTouchHelper 實現互動動畫

到這裡,最開始的所有互動效果都已經實現了。

最後附上Demo連結

github.com/GitLqr/Mate…

相關文章