以下是RecyclerView結合ItemTouchHelper實作的清單和網格布局的拖拽效果。
效果圖如下:(gif圖有點頓卡,其實運作是很流暢的)
demo下載下傳位址:
DragRecyclerView
如何實作
那麼是如何實作的呢?主要就要使用到ItemTouchHelper ,ItemTouchHelper 是support-v7包中加入的一個幫助開發人員處理拖拽和滑動的實作類,它能夠讓你非常容易實作側滑删除、拖拽的功能。
我們隻需要執行個體化一個ItemTouchHelper,然後關聯到RecyclerView就OK了:
itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback());
itemTouchHelper.attachToRecyclerView(recyclerView);
- 1
- 2
構造方法中需要一個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) {
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
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 = ;
} else {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
final int swipeFlags = ;
}
return makeMovementFlags(dragFlags, swipeFlags);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
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 + );
}
} else {
for (int i = fromPosition; i > toPosition; i--) {
Collections.swap(results, i, i - );
}
}
adapter.notifyItemMoved(fromPosition, toPosition);
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
同理如果我們設定了非0的swipeFlags,我們在滑動item的時候就會回調onSwiped的方法,我們不處理這個事件,空着就行了。
到這裡,已經可以拖拽了,但是拖拽的時候我們拖拽的對象不能高亮顯示,這是不友好的,我們希望拖拽的Item在拖拽的過程中背景顔色加深,這樣就需要繼續重寫下面兩個方法:
//當長按選中item的時候(拖拽開始的時候)調用
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
}
//當手指松開的時候(拖拽完成的時候)調用
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
我們在開始拖拽的時候給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();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
OK,這樣就完成了一個可拖拽的GridView。
更加複雜的需求
上面的代碼完成了基本功能,但實際的産品需要往往可能會有些不一樣,比如說,産品希望,有一些item可以拖拽,一些item無法拖拽,就如上圖的“更多”是無法拖拽的。這個咋辦呢?
其實在上面我們實作的Callback類中有一個方法我們沒有重寫:
@Override
public boolean isLongPressDragEnabled() {
return true;
}
- 1
- 2
- 3
- 4
這個方法是為了告訴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;
}
- 1
- 2
- 3
- 4
接着我們給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()-) {
itemTouchHelper.startDrag(vh);
}
}
//item 點選事件
@Override
public void onItemClick(RecyclerView.ViewHolder vh) {
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
OK,大功告成。
額外的功能
儲存位置
關閉頁面以後再打開,又恢複到了初始化的位置,是以就需要儲存調整的位置到本地,下次初始化的時候讀取位置。
儲存位置應該由開發者自己實作,因為每個人本地化資料的方式都不一樣,我這裡做一個簡單的實作,使用了開源的ACache類,兩個方法,搞定:
//讀取
ACache.get(context).getAsObject("items");
//存儲
ACache.get(context).put("items",results);
- 1
- 2
- 3
- 4
在clearView方法(拖拽完成)中調用存儲方法,在頁面初始化資料是調用讀取方法。詳見demo
開始拖拽時震動
支付寶的拖拽網格在長按後開始拖拽時會有一次短時間的震動提示使用者開始拖拽了,很友好的互動,我們也加一個:
添權重限:
<uses-permission android:name="android.permission.VIBRATE" />
- 1
在開始拖拽時添加下面代碼:
//擷取系統震動服務
Vibrator vib = (Vibrator) activity.getSystemService(Service.VIBRATOR_SERVICE);
//震動毫秒
vib.vibrate();
- 1
- 2
- 3
- 4
詳見demo,demo下載下傳位址:
DragRecyclerView