以下是RecyclerView結合ItemTouchHelper實現的列表和網格佈局的拖拽效果。
效果圖如下:(gif圖有點頓卡,其實運行是很流暢的)
demo下載地址:
DragRecyclerView
如何實現
那麼是如何實現的呢?主要就要使用到ItemTouchHelper ,ItemTouchHelper 是support-v7包中加入的一個幫助開發人員處理拖拽和滑動的實現類,它能夠讓你非常容易實現側滑刪除、拖拽的功能。
我們只需要實例化一個ItemTouchHelper,然後關聯到RecyclerView就OK了:
itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback());
itemTouchHelper.attachToRecyclerView(recyclerView);
構造方法中需要一個ItemTouchHelper.Callback,ItemTouchHelper會在拖拽或剔除的時候回調Callback中相應的方法,我們只需在Callback中實現自己的邏輯就可以了。
自定義一個類繼承實現ItemTouchHelper.Callback接口,需要實現以下方法:
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}
getMovementFlags用於設置是否處理拖拽事件和滑動事件,以及拖拽和滑動操作的方向,比如如果是列表類型的RecyclerView,拖拽只有UP、DOWN兩個方向,而如果是網格類型的則有UP、DOWN、LEFT、RIGHT四個方向:
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
final int swipeFlags = 0;
} else {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
final int swipeFlags = 0;
}
return makeMovementFlags(dragFlags, swipeFlags);
}
dragFlags 是拖拽標誌,swipeFlags是滑動標誌,我們把swipeFlags 都設置爲0,表示不處理滑動操作。
如果我們設置了非0的dragFlags ,那麼當我們長按item的時候就會進入拖拽並在拖拽過程中不斷回調onMove()方法,我們就在這個方法裏獲取當前拖拽的item和已經被拖拽到所處位置的item的ViewHolder,有了這2個ViewHolder,我們就可以交換他們的數據集並調用Adapter的notifyItemMoved方法來刷新item。
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getAdapterPosition();//得到拖動ViewHolder的position
int toPosition = target.getAdapterPosition();//得到目標ViewHolder的position
if (fromPosition < toPosition) {
for (int i = fromPosition; i < toPosition; i++) {
Collections.swap(results, i, i + 1);
}
} else {
for (int i = fromPosition; i > toPosition; i--) {
Collections.swap(results, i, i - 1);
}
}
adapter.notifyItemMoved(fromPosition, toPosition);
return true;
}
同理如果我們設置了非0的swipeFlags,我們在滑動item的時候就會回調onSwiped的方法,我們不處理這個事件,空着就行了。
到這裏,已經可以拖拽了,但是拖拽的時候我們拖拽的對象不能高亮顯示,這是不友好的,我們希望拖拽的Item在拖拽的過程中背景顏色加深,這樣就需要繼續重寫下面兩個方法:
//當長按選中item的時候(拖拽開始的時候)調用
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
}
//當手指鬆開的時候(拖拽完成的時候)調用
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
}
我們在開始拖拽的時候給item添加一個背景色,然後在拖拽完成的時候還原:
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
viewHolder.itemView.setBackgroundColor(Color.LTGRAY);
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setBackgroundColor(0);
}
OK,這樣就完成了一個可拖拽的GridView。
更加複雜的需求
上面的代碼完成了基本功能,但實際的產品需要往往可能會有些不一樣,比如說,產品希望,有一些item可以拖拽,一些item無法拖拽,就如上圖的“更多”是無法拖拽的。這個咋辦呢?
其實在上面我們實現的Callback類中有一個方法我們沒有重寫:
@Override
public boolean isLongPressDragEnabled() {
return true;
}
這個方法是爲了告訴ItemTouchHelper是否需要RecyclerView支持長按拖拽,默認返回是ture(即支持),理所當然我們要支持,所以我們沒有重寫,因爲默認true。但是這樣做是默認全部的item都可以拖拽,怎麼實現部分item拖拽呢,查閱isLongPressDragEnabled方法的源碼發現,上面的註釋上寫着:
Default value returns true but you may want to disable this if you want to start
dragging on a custom view touch using {@link #startDrag(ViewHolder)}.
意思是如果你想自定義觸摸view,那麼就使用startDrag(ViewHolder)方法。
原來如此,我們可以在item的長按事件中得到當前item的ViewHolder ,然後調用ItemTouchHelper.startDrag(ViewHolder vh)就可以實現拖拽了,那就這麼辦:
首先我們重寫isLongPressDragEnabled返回false,我們要自己調用拖拽過程:
@Override
public boolean isLongPressDragEnabled() {
return false;
}
接着我們給RecyclerView添加item長按事件,判斷item是否是最後一個(最後一個是“更多”),不是則開始拖拽。
但是,我們都知道RecyclerView並沒有提供OnItemLongClickListener,這個問題我在上一篇博客中已經完美地解決了,就是使用OnItemTouchListener,然後識別觸摸手勢,這裏給上傳送門:RecyclerView無法添加onItemClickListener最佳的高效解決方案,後面我就直接使用上一篇的成果,不重複講了:
recyclerView.addOnItemTouchListener(new OnRecyclerItemClickListener(recyclerView) {
//item 長點擊事件
@Override
public void onLongClick(RecyclerView.ViewHolder vh) {
//如果item不是最後一個,則執行拖拽
if (vh.getLayoutPosition()!=results.size()-1) {
itemTouchHelper.startDrag(vh);
}
}
//item 點擊事件
@Override
public void onItemClick(RecyclerView.ViewHolder vh) {
}
});
OK,大功告成。
額外的功能
保存位置
關閉頁面以後再打開,又恢復到了初始化的位置,所以就需要保存調整的位置到本地,下次初始化的時候讀取位置。
保存位置應該由開發者自己實現,因爲每個人本地化數據的方式都不一樣,我這裏做一個簡單的實現,使用了開源的ACache類,兩個方法,搞定:
//讀取
ACache.get(context).getAsObject("items");
//存儲
ACache.get(context).put("items",results);
在clearView方法(拖拽完成)中調用存儲方法,在頁面初始化數據是調用讀取方法。詳見demo
開始拖拽時震動
支付寶的拖拽網格在長按後開始拖拽時會有一次短時間的震動提示用戶開始拖拽了,很友好的交互,我們也加一個:
添加權限:
<uses-permission android:name="android.permission.VIBRATE" />
在開始拖拽時添加下面代碼:
//獲取系統震動服務
Vibrator vib = (Vibrator) activity.getSystemService(Service.VIBRATOR_SERVICE);
//震動70毫秒
vib.vibrate(70);