這次主要是把 RecyclerView 比較常用的基本的點,在這裡集中整理一下。從這篇文章主要梳理以下幾點:
- 優雅的實現:item 點選事件 & item 長點選事件
- RecyclerView 新增 divider 的標準姿勢
- RecyclerView 實現 item 的拖曳排序和滑動刪除
- 拖曳排序時,限制首個 item 固定的實現
先看一下最終的效果圖:
自從 RecyclerView 釋出以來,由於其高度的可互動性被廣泛使用。相信大家肯定對它的使用方法已經非常熟練了,今天主要是為大家總結一下較正常用法更加優雅的方式。
如果你想再回顧一下 RecyclerView 的基本使用方法,推薦鴻洋的這篇文章:
Android RecyclerView 使用完全解析 體驗藝術般的控制元件
優雅的實現:item 點選事件 & item 長點選事件
使用方式
RecyclerView 的 api 雖然沒有提供 onItemClickListener 但是提供了 addOnItemTouchListener() 方法,既然可以新增觸控監聽,那麼我們完全可以獲取觸控手勢來識別點選事件,然後通過觸控座標來判斷點選的是哪一個item。
mRecyclerView.addOnItemTouchListener(new OnRecyclerItemClickListener(mRecyclerView) {
@Override
public void onItemClick(RecyclerView.ViewHolder viewHolder) {
//TODO item 點選事件
}
@Override
public void onLongClick(RecyclerView.ViewHolder viewHolder) {
//TODO item 長按事件
}
});
複製程式碼
其中 OnRecyclerItemClickListener 是自定義的一個觸控監聽器,程式碼如下:
public abstract class OnRecyclerItemClickListener implements RecyclerView.OnItemTouchListener{
private GestureDetectorCompat mGestureDetectorCompat;//手勢探測器
private RecyclerView mRecyclerView;
public OnRecyclerItemClickListener(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
mGestureDetectorCompat = new GestureDetectorCompat(mRecyclerView.getContext(),
new ItemTouchHelperGestureListener());
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
mGestureDetectorCompat.onTouchEvent(e);
return false;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
mGestureDetectorCompat.onTouchEvent(e);
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
public abstract void onItemClick(RecyclerView.ViewHolder viewHolder);
public abstract void onLongClick(RecyclerView.ViewHolder viewHolder);
}
複製程式碼
GestureDetectorCompat 中傳入了一個 ItemTouchHelperGestureListener,程式碼如下:
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener{
//一次單獨的輕觸抬起手指操作,就是普通的點選事件
@Override
public boolean onSingleTapUp(MotionEvent e) {
View childViewUnder = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
if (childViewUnder != null) {
RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(childViewUnder);
onItemClick(childViewHolder);
}
return true;
}
//長按螢幕超過一定時長,就會觸發,就是長按事件
@Override
public void onLongPress(MotionEvent e) {
View childViewUnder = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
if (childViewUnder != null) {
RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(childViewUnder);
onLongClick(childViewHolder);
}
}
}
複製程式碼
原理分析
上面的程式碼很簡單沒什麼複雜的地方,就是通過一個手勢探測器 GestureDetectorCompat 來探測螢幕事件,然後通過手勢監聽器 SimpleOnGestureListener 來識別手勢事件的種類,然後呼叫我們設定的對應的回撥方法。這裡值得說的是:當獲取到了 RecyclerView 的點選事件和觸控事件資料 MotionEvent,那麼如何才能知道點選的是哪一個 item 呢?
RecyclerView已經為我們提供了這樣的方法:findChildViewUnder()
。
我們可以通過這個方法獲得點選的 item ,同時我們呼叫 RecyclerView 的另一個方法 getChildViewHolder()
,可以獲得該 item 的 ViewHolder,最後再回撥我們定義的虛方法 onItemClick() 就ok了,這樣我們就可以在外部實現該方法來獲得 item 的點選事件了。
RecyclerView 新增 divider 的標準姿勢
當你想給條目間新增 divider 時,你可能自然而然的去嘗試這種方式:
<android.support.v7.widget.RecyclerView
android:divider="#ffff0000"
android:dividerHeight="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
複製程式碼
其實 RecyclerView 是沒有這兩個屬性的,就算你寫上也不會有任何效果。
當然你還可以通過給 item 的最外層佈局設定一個 margin 值,甚至你還可以專門在 item 佈局中的適當地方新增一個高度/寬度為 1 的帶背景的 View 作為 divider,這兩種方法呢,確實有效果,但是不夠優雅,有時還可能帶來一些想不到的問題。
其實官方還是為我們提供了為 RecyclerView 新增分割線的方式的,那就是方法: mRecyclerView.addItemDecoration()
。該方法的引數為 RecyclerView.ItemDecoration,該類為抽象類,且官方目前並沒有提供預設的實現類,我們只能自己來實現。
使用方式
列表佈局的分割線例項:
public class DividerListItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider
};
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
private Drawable mDivider;
private int mOrientation;
public DividerListItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
public DividerListItemDecoration(Context context, int orientation, int drawableId) {
mDivider = ContextCompat.getDrawable(context, drawableId);
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
//畫線 > 就是畫出你想要的分割線樣式
@Override
public void onDraw(Canvas c, RecyclerView parent) {
if (mOrientation == VERTICAL_LIST) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
android.support.v7.widget.RecyclerView v = new android.support.v7.widget.RecyclerView(parent.getContext());
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
//設定條目周邊的偏移量
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
複製程式碼
網格佈局分割線例項:
public class DividerGridItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
private Drawable mDivider;
private int lineWidth = 1;
public DividerGridItemDecoration(Context context) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
}
public DividerGridItemDecoration(int color) {
mDivider = new ColorDrawable(color);
}
public DividerGridItemDecoration() {
this(Color.parseColor("#cccccc"));
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
drawHorizontal(c, parent);
drawVertical(c, parent);
}
private int getSpanCount(RecyclerView parent) {
// 列數
int spanCount = -1;
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
spanCount = ((StaggeredGridLayoutManager) layoutManager)
.getSpanCount();
}
return spanCount;
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getLeft() - params.leftMargin;
final int right = child.getRight() + params.rightMargin
+ lineWidth;
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + lineWidth;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public void drawVertical(Canvas c, RecyclerView parent) {
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getTop() - params.topMargin;
final int bottom = child.getBottom() + params.bottomMargin;
final int left = child.getRight() + params.rightMargin;
final int right = left + lineWidth;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
private boolean isLastColum(RecyclerView parent, int pos, int spanCount, int childCount) {
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
if ((pos + 1) % spanCount == 0)// 如果是最後一列,則不需要繪製右邊
{
return true;
}
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int orientation = ((StaggeredGridLayoutManager) layoutManager)
.getOrientation();
if (orientation == StaggeredGridLayoutManager.VERTICAL) {
if ((pos + 1) % spanCount == 0)// 如果是最後一列,則不需要繪製右邊
{
return true;
}
} else {
childCount = childCount - childCount % spanCount;
if (pos >= childCount)// 如果是最後一列,則不需要繪製右邊
return true;
}
}
return false;
}
private boolean isLastRaw(RecyclerView parent, int pos, int spanCount, int childCount) {
LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
childCount = childCount - childCount % spanCount;
if (pos >= childCount)// 如果是最後一行,則不需要繪製底部
return true;
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int orientation = ((StaggeredGridLayoutManager) layoutManager)
.getOrientation();
// StaggeredGridLayoutManager 且縱向滾動
if (orientation == StaggeredGridLayoutManager.VERTICAL) {
childCount = childCount - childCount % spanCount;
// 如果是最後一行,則不需要繪製底部
if (pos >= childCount)
return true;
} else
// StaggeredGridLayoutManager 且橫向滾動
{
// 如果是最後一行,則不需要繪製底部
if ((pos + 1) % spanCount == 0) {
return true;
}
}
}
return false;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
boolean b = state.willRunPredictiveAnimations();
int itemPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
int spanCount = getSpanCount(parent);
int childCount = parent.getAdapter().getItemCount();
// if (isLastRaw(parent, itemPosition, spanCount, childCount))// 如果是最後一行,則不需要繪製底部
// {
// outRect.set(0, 0, lineWidth, 0);
// }
// else if (isLastColum(parent, itemPosition, spanCount, childCount))// 如果是最後一列,則不需要繪製右邊
// {
//// if (b){
//// outRect.set(0, 0, lineWidth, lineWidth);
//// }else {
// outRect.set(0, 0, 0, lineWidth);
//// }
// }
// else {
outRect.set(0, 0, lineWidth, lineWidth);
// }
}
}
複製程式碼
使用說明
上面給出的兩個例項都是最簡單的一條線的分割。這裡的分割線你是可以自由的去自定義它的,具體如何實現也不是太複雜,這裡不再做詳細介紹了,推薦一篇文章:
RecyclerView 實現 item 的拖曳排序和滑動刪除
下面就主要為大家梳理一下拖曳排序和滑動刪除的實現,具體實現效果看文章首部效果圖,這裡就不再重複放圖了。
實現方式
主要就要使用到 ItemTouchHelper,ItemTouchHelper 一個幫助開發人員處理拖拽和滑動刪除的實現類,它能夠讓你非常容易實現側滑刪除、拖拽的功能。(ItemTouchHelper 的使用並不僅僅侷限於 RecyclerView 的滑動刪除,你同意可以用在其他需要拖曳滑動的地方。當然,今天我們不涉及其他地方的使用)
實現的程式碼並關聯到 RecyclerView 非常簡單,程式碼如下:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback());
itemTouchHelper.attachToRecyclerView(mRecyclerView);
複製程式碼
程式碼很簡單,沒什麼好說的。需要我們關注的是建立 ItemTouchHelper 時傳入的引數 ItemTouchHelper.Callback() 。ItemTouchHelper 會在拖拽的時候回撥 Callback 中相應的方法,我們只需在 Callback 中實現自己的邏輯。
自定義一個類繼承實現 ItemTouchHelper.Callback 介面,需要實現以下方法:
//通過返回值來設定是否處理某次拖曳或者滑動事件
public abstract int getMovementFlags(RecyclerView recyclerView,
ViewHolder viewHolder);
//當長按並進入拖曳狀態時,拖曳的過程中不斷的回撥此方法
public abstract boolean onMove(RecyclerView recyclerView,
ViewHolder viewHolder, ViewHolder target);
//滑動刪除的回撥
public abstract void onSwiped(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) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN |
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
} else {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
}
}
複製程式碼
dragFlags 是拖拽標誌,
swipeFlags 是滑動標誌,
swipeFlags 都設定為0,暫時不考慮滑動相關操作。
如果設定了相關的 dragFlags,那麼當長按 item 的時候就會進入拖拽並在拖拽過程中不斷回撥 onMove() 方法,我們就在這個方法裡獲取當前拖拽的 item 和已經被拖拽到所處位置的 item 的ViewHolder,有了這2個 ViewHolder,我們就可以交換他們的資料集並呼叫 Adapter 的notifyItemMoved 方法來重新整理 item。
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
//拖動的 item 的下標
int fromPosition = viewHolder.getAdapterPosition();
//目標 item 的下標,目標 item 就是當拖曳過程中,不斷和拖動的 item 做位置交換的條目。
int toPosition = target.getAdapterPosition();
if (fromPosition < toPosition) {
for (int i = fromPosition; i < toPosition; i++) {
Collections.swap(((RecyAdapter) mAdapter).getDataList(), i, i + 1);
}
} else {
for (int i = fromPosition; i > toPosition; i--) {
Collections.swap(((RecyAdapter) mAdapter).getDataList(), i, i - 1);
}
}
mAdapter.notifyItemMoved(fromPosition, toPosition);
return true;
}
複製程式碼
只要重寫完上面這兩個方法,RecyclerView 就能實現拖曳的效果了。是不是很簡單?但是雖然拖曳是沒什麼問題了,但是並不能達到下圖的效果,因為你正在拖曳的 item 並沒有陰影效果。
那怎麼才能實現被拖曳的 item 有背景顏色加深起到強調的視覺效果呢?這是需要重寫下面兩個方法:
//當長按 item 剛開始拖曳的時候呼叫
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
//給被拖曳的 item 設定一個深顏色背景
viewHolder.itemView.setBackgroundColor(Color.LTGRAY);
}
super.onSelectedChanged(viewHolder, actionState);
}
//當完成拖曳手指鬆開的時候呼叫
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
//給已經完成拖曳的 item 恢復開始的背景。
//這裡我們設定的顏色儘量和你 item 在 xml 中設定的顏色保持一致
viewHolder.itemView.setBackgroundColor(Color.WHITE);
}
複製程式碼
這樣就能完全達到上面圖片的效果了。
滑動刪除
如何實現滑動刪除呢?我們只需要實現第三個方法 onSwipe() 就行了。程式碼如下:
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int adapterPosition = viewHolder.getAdapterPosition();
mAdapter.notifyItemRemoved(adapterPosition);
((RecyAdapter) mAdapter).getDataList().remove(adapterPosition);
}
複製程式碼
同時也不要忘了修改一下 getMovementFlags() 方法,以便能夠相應滑動事件:
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN |
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
} else {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
//注意:和拖曳的區別就是在這裡
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
}
複製程式碼
那目前你就能完美的實現拖曳排序和滑動刪除了。
拖曳排序,首個固定
有時我們希望首個 item 不能被拖曳排序。比如我們在新聞 App 中常見當我們進行新聞分類時,“熱門”新聞這個分類總是第一個且不能被拖曳修改,類似下面的效果:
那麼怎麼才能達到上面的效果呢?在上面我們的 Callback 類中有一個方法:
public boolean isLongPressDragEnabled() {
return true;
}
複製程式碼
這個方法是為了告訴 ItemTouchHelper 是否需要 RecyclerView 支援長按拖拽,預設返回是 ture,理所當然我們要支援,所以我們沒有重寫,因為預設true。但是這樣做是預設全部的item都可以拖拽,怎麼實現部分item拖拽呢,在 isLongPressDragEnabled 方法的原始碼中有提示說,如果想自定義拖曳 view,那麼就使用 startDrag(ViewHolder) 方法。
**第一步:**那麼我們就先重寫 isLongPressDragEnabled() 方法,返回 false 讓它控制所有的 item 都不能拖曳。
public boolean isLongPressDragEnabled() {
return false;
}
複製程式碼
**第二步:**我們給 RecyclerView 設定 item 的長按監聽事件,然後判斷這個 item 是不是第一個(或者最後一個,如果你不想讓最後一個被拖曳的話),如果不是我們就手動呼叫 startDrag(ViewHolder) 讓 item 開始被拖曳。
結合上面我們提供的給 item 設定點選和長按事件的方法,我們可以這樣:
mRecyclerView.addOnItemTouchListener(new OnRecyclerItemClickListener(mRecyclerView) {
@Override
public void onItemClick(RecyclerView.ViewHolder viewHolder) {
//TODO:點選事件
}
@Override
public void onLongClick(RecyclerView.ViewHolder viewHolder) {
//當 item 被長按且不是第一個時,開始拖曳這個 item
if (viewHolder.getLayoutPosition() != 0) {
itemTouchHelper.startDrag(viewHolder);
}
}
});
複製程式碼
**第三步:**如果你以為上面兩步你就達到首個 item 固定不被拖曳的話,恭喜你,答對了!首個 item 確實固定不能被拖曳了,可是看看下圖,就會令你大跌眼睛:
雖然我們通過上面兩步控制了首個 item 不能被長按拖曳,但是我們並沒有處理,別的 item 被拖曳到首個 item 的情況。那麼如何才能讓首個 item 不被擠掉呢,這個也很簡單,只需要在 Callback 的 onMove() 方法中處理首個 item 被當著目標 item 的情況就行了。
@Override
public boolean onMove(...) {
int fromPosition = viewHolder.getAdapterPosition();
int toPosition = target.getAdapterPosition();
//其他地方程式碼都和上面的一樣,這個就直接省略了
//這裡判斷如果目標 item 是首個 item,那麼就直接返回false,表示不響應此次拖曳移動
if (toPosition == 0) {
return false;
}
...
return true;
}
複製程式碼
好了,到這裡就大功告成了。
本文原始碼地址:github.com/OCNYang/Rec…
參考文章:
chuansong.me/n/400690551…
chuansong.me/n/400690851…
www.10tiao.com/html/227/20…