RecyclerView 知識梳理(5) ItemTouchHelper

澤毛發表於2017-12-21

一、概述

ItemTouchHelperRecyclerView的整個體系中,負責監聽Item的手勢操作,我們通過給它設定一個繼承於ItemTouchHelper.Callback的子類,在其中處理ItemUI變化,就可以完成側滑刪除、拖動排序等操作,下面,我們分以下幾部介紹:

  • API解析
  • 實戰
  • 採用預設動畫
  • 自定義側滑刪除動畫

二、API分析

對於Item的手勢操作分為兩種:側滑和拖動,如果需要支援這兩種,那麼需要給ItemTouchHelper傳入一個ItemTouchHelper.Callback的子類,並把ItemTouchHelperRecyclerView關聯起來,下面,我們先來介紹一下ItemTouchHelper.Callback個回撥方法的含義:

控制相關

  • public boolean isLongPressDragEnabled() 是否可以通過長按來觸發拖動操作,預設返回true,如果返回false,那麼可以通過startDrag(ViewHolder)方法來觸發某個特定Item的拖動的機制。
  • public boolean isItemViewSwipeEnabled() 是否可以對每個Item進行側滑。
  • public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) 返回對於某個ViewHolder可以移動的方向,可選的值有UP/DOWN/LEFT/RIGHT/START/END。對於縱向排列的線性佈局而言,如果要支援上下拖動排序,那麼就要標誌位中就要包含UP&DOWN,而如果需要支援左滑刪除,那麼標誌位中就要包含LEFT

結果相關

  • public abstract boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) 當某個被拖動的Item被從舊位置拖動到了新位置後回撥,如果返回true,那麼ItemTouchHelper就認為viewHolder已經被移動到了targetAdapter中的位置。
  • public abstract void onSwiped(ViewHolder viewHolder, int direction) 當某個Item被滑動到消失時回撥,direction表示滑動的方向。

狀態相關

  • public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)Item的狀態發生改變時,回撥該方法,actionState的取值有ACTION_STATE_IDLE/ACTION_STATE_SWIPE/ACTION_STATE_DRAG

  • public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) 標誌著使用者對於某個Item的操作並且Item的動畫結束,此時我們應該恢復它的狀態,以保證它被重新使用的時候能正確地展現。

  • public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)

  • Canvas:繪製RecyclerViewCanvas

  • dx, dy:偏移。

  • actionState:拖拽還是側滑,對應ACTION_STATE_DRAGACTION_STATE_SWIPE

  • isCurrentlyActivetrue表示這個Item正在被使用者所控制,false則表示它僅僅是在回到原本狀態的動畫過程當中。

  • public void onChildDrawOver(Canvas c, RecyclerView recyclerView, ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) 和上面類似,只不過它是繪製在Item之上。

三、實戰

3.1 使用系統預設效果

如果我們希望使用系統預設的效果,那麼只需要做以下幾步:

  • 繼承於ItemTouchHelper.Callback編寫自己的回撥類,並在拖動和側滑操作完成之後更新資料:
public class SimpleItemTouchHelper extends ItemTouchHelper.Callback {

    private ItemTouchAdapter mAdapter;

    public SimpleItemTouchHelper(ItemTouchAdapter adapter) {
        mAdapter = adapter;
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        Log.d("SimpleItemTouchHelper", "onSwiped, onMove, source=" + viewHolder.getAdapterPosition() + ",target=" + target.getAdapterPosition());
        mAdapter.onItemDragged(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        Log.d("SimpleItemTouchHelper", "onSwiped, direction=" + direction);
        mAdapter.onItemSwiped(viewHolder.getAdapterPosition());
    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
    }
}
複製程式碼
  • 編寫資料操作的程式碼:
public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.NormalViewHolder> implements ItemTouchAdapter {

    //......

    @Override
    public void onItemDragged(int from, int to) {
        Collections.swap(mTitles, from, to);
        notifyItemMoved(from, to);
    }

    @Override
    public void onItemSwiped(int position) {
        mTitles.remove(position);
        notifyItemRemoved(position);
    }
}
複製程式碼
  • ItemTouchHelper.CallbackRecyclerView關聯起來,看註釋中的1,2,3步:
    private void init() {
        List<String> titles = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            titles.add(String.valueOf(i));
        }
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_content);
        recyclerView.setLayoutManager(layoutManager);
        NormalAdapter adapter = new NormalAdapter(titles);
        //1.自定義的ItemTouchHeloer.Callback
        SimpleItemTouchHelper simpleItemTouchHelper = new 
SimpleItemTouchHelper(adapter);
        //2.利用這個Callback構造ItemTouchHelper
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchHelper);
        //3.把ItemTouchHelper和RecyclerView關聯起來.
        itemTouchHelper.attachToRecyclerView(recyclerView);
        recyclerView.setAdapter(adapter);
    }
複製程式碼

下面就是最終的效果:

RecyclerView 知識梳理(5)   ItemTouchHelper

3.2 自定義側滑刪除動畫

當我們需要自定側滑刪除動畫時,那麼需要重寫onChildDraw或者onChildDrawOver方法,在其中監聽滑動距離的變化,並根據它來實時改變viewHolder中的UI,首先看效果:

RecyclerView 知識梳理(5)   ItemTouchHelper

  • 首先,我們需要重寫Item的佈局,它包含兩層,頂層是普通狀態的標題文案,而底層則是藍色底的刪除提示:
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="66dp">
    <!-- 刪除提示 -->
    <LinearLayout
        android:id="@+id/ll_delete"
        android:orientation="vertical"
        android:gravity="center"
        android:layout_gravity="end"
        android:background="@android:color/holo_blue_dark"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:layout_width="wrap_content"
        android:layout_height="match_parent">
        <ImageView
            android:src="@android:drawable/ic_input_delete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:text="delete"
            android:textColor="@android:color/white"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <!-- 普通文案 -->
    <TextView
        android:id="@+id/tv_title"
        android:gravity="center"
        android:background="@android:color/white"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</FrameLayout>
複製程式碼

接著,我們需要重寫ItemTouchHelper.Callback

public class AdvancedItemTouchHelper extends ItemTouchHelper.Callback {

    private ItemTouchAdapter mAdapter;

    public AdvancedItemTouchHelper(ItemTouchAdapter itemTouchAdapter) {
        mAdapter = itemTouchAdapter;
    }

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

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        return makeMovementFlags(0, ItemTouchHelper.LEFT);
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        mAdapter.onItemSwiped(viewHolder.getAdapterPosition());
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        return false;
    }

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        ((NormalAdapter.NormalViewHolder) viewHolder).mTv.setTranslationX(0);
    }

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        NormalAdapter.NormalViewHolder mViewHolder = (NormalAdapter.NormalViewHolder) viewHolder;
        int deleteWidth = mViewHolder.mDeleteLayout.getWidth();
        float fraction = deleteWidth / (float) mViewHolder.itemView.getWidth();
        mViewHolder.mTv.setTranslationX(dX * fraction);
    }
}
複製程式碼

這裡面有幾點需要注意:

  • 為了讓Item支援左滑刪除,我們需要在getMovementFlags中返回ItemTouchHelper.LEFT標誌位。
  • onChildDraw當中,通過傳入的dX動態改變了普通文案的translationX,使得底層的刪除提示能夠漏出。
  • 在側滑操作完成之後,通過Adapter來刪除資料。
  • clearView中,需要把mTv重置為初始的狀態。

最後,我們按照前面的方法,把它和RecyclerView關聯起來:

    private void init() {
        List<String> titles = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            titles.add("Item " + String.valueOf(i));
        }
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_content);
        recyclerView.setLayoutManager(layoutManager);
        NormalAdapter adapter = new NormalAdapter(titles);
        AdvancedItemTouchHelper advancedItemTouchHelper = new AdvancedItemTouchHelper(adapter);
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(advancedItemTouchHelper);
        itemTouchHelper.attachToRecyclerView(recyclerView);
        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
        recyclerView.setAdapter(adapter);
    }
複製程式碼

四、小結

自定義RecyclerView的手勢動畫,關鍵是要理解ItemTouchHelper.Callback中各回撥函式的含義,再通過回撥函式中傳入的數值來動態改變viewHolder中儲存的itemView以及其子View的展現形式,就可以做出各種絢麗的效果。

五、參考文獻

RecyclerView 進階:使用 ItemTouchHelper 實現拖拽和側滑刪除


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章