RecyclerView 是伴隨著 android5.x 出來的控件,第一次提出應該是在14年的 Google I/O 大會(猜測,懶得查,反正我不 care 它是什么時候出來的),到現在17年 Google I/O 大會結束正好三年,相信大家都早已經把 RecyclerView 使用到項目當中了。
我們都知道,RecyclerView 的出現,是為了取代 ListView、GridView 而出現的。記得有次面試的時候,面試官問我為什么要使用 RecyclerView,你 RecyclerView 能實現的列表,我 ListView 同樣可以實現,我當時是這樣回答的:整體上看RecyclerView架構,提供了一種插拔式的體驗,高度的解耦,異常的靈活
,這段話我隨便在網上復制的,大意差不多。好了,扯遠了。
為什么說 RecyclerView 讓我歡喜讓我憂?
歡喜因為用了 RecyclerView 之后,感覺就再也不想去寫 ListView 了;憂則是因為盡管用了幾年 RecyclerView,但直到現在感覺 RecyclerView 還是玩不溜,同感玩不溜的同學請握個抓。-
為什么說 RecyclerView 高度解耦
我們來看看 RecyclerView 的幾個大家熟悉的方法:mRecyclerView.setLayoutManager(layout);
//設置條目布局規則
mRecyclerView.setItemAnimator();
//設置條目動畫
mRecyclerView.addItemDecoration();
//自定義條目裝飾
mItemTouchHelper.attachToRecyclerView(mRecyclerView);
//定制條目觸摸
現在我們來回顧一下,對應的這幾個規則,我們的ListView 是怎么實現的, layoutmanger:ListView 并不支持這個功能
ItemAnimation:在 Adapter的 getView()方法里面給創造出來的 View 加動畫
ItemDecoration:在 Adapter 的 getView()方法里面自行處理
ItemTouchHelp: 在 Adapyer 的 getView()方法里面自行給 View 設置 Touch 事件處理。
對比一下,瞬間感覺 ListView 弱爆了。。。
說了這么多優點,說說缺點,既然 RecyclerView 給我們提供了這么多擴展,可以高度定制,但是上手難度同樣增加了不止一個等級。還有條目點擊事件,簡直喪心病狂,竟然接口都沒提供。
好了,我們要開始寫代碼了。
一、RecyclerView 的基本使用
先實現一個簡單的列表:
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycle_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(new RecyclerView.Adapter() {
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//創建一個 ViewHolder 并且返回
return null;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
//在這里給 ViewHolder 綁定數據
}
@Override
public int getItemCount() {
//控制RecyclerView的條目數
return 0;
}
});
這樣我們就使用RecyclerView實現了一個簡單的列表,由于代碼比較簡單,我直接一筆帶過了。這里對比我們的 Listview,就多了一行代碼recyclerView.setLayoutManager(new LinearLayoutManager(this));
,至于這行代碼具體有什么作用呢,我們后面再說。
二、封裝RecyclerView 的 Adapter
Adapter是 RecycleView 中最重要的一個類,雖然沒有特別復雜難理解的代碼,但是 RecycleView 的刷新條目、多條目、點擊事件都需要在里面處理。而且每一個 RecycleView 的 Adapter 幾乎都需要帶讀寫,所以這是一個高頻率、代碼量稍多的一個類,因此我們在使用的過程中一般會對 Adapter 進行一下封裝。
- 數據處理用泛型規范輸入數據
我們在實際項目開發當中,會有很多地方都用到 RecycleView(太長了,下面我用 rv 簡稱吧),而且數據的結構各有不同,因此,在 BaseAdapter 里面,我們需要用一個泛型 T 去規范數據結構的類型,避免誤操作。
- 默認實現條目的增刪方法和getItemCount();
這個比較簡單,等下直接看代碼
- 配合 ButtonKnife,簡化 ViewHolder 里面的 findViewbyid 操作
這個也簡單,抽取了一個 BaseViewHolder,在構造方法里面綁定的。
- 好像也沒什么好寫的,我直接貼我項目中封裝的 BaseAdapter 的代碼吧
public abstract class BaseAbstractAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
protected final String TAG = getClass().getSimpleName();
protected final Context mContext;
protected final LayoutInflater mLayoutInflater;
protected List<T> mDataList = new ArrayList<>();
public BaseAbstractAdapter(Context context) {
this.mContext = context;
this.mLayoutInflater = LayoutInflater.from(mContext);
}
public Context getContext() {
return mContext;
}
public List<T> getDataList() {
return mDataList;
}
public T getItemData(int position) {
return (position >= 0 && position < mDataList.size()) ? mDataList.get(position) : null;
}
@Override
public int getItemCount() {
return mDataList == null ? 0 : mDataList.size();
}
/**
* 移除某一條記錄
*
* @param position 移除數據的position
*/
public void removeItem(int position) {
if (position >= 0 && position < mDataList.size()) {
mDataList.remove(position);
notifyItemRemoved(position);
}
}
/**
* 添加一條記錄
*
* @param data 需要加入的數據結構
* @param position 插入位置
*/
public void addItem(T data, int position) {
if (position >= 0 && position <= mDataList.size()) {
mDataList.add(position, data);
notifyItemInserted(position);
}
}
/**
* 添加一條記錄
*
* @param data 需要加入的數據結構
*/
public void addItem(T data) {
addItem(data, mDataList.size());
}
/**
* 移除所有記錄
*/
public void clearItems() {
int size = mDataList.size();
if (size > 0) {
mDataList.clear();
notifyItemRangeRemoved(0, size);
}
}
/**
* 批量添加記錄
*
* @param data 需要加入的數據結構
* @param position 插入位置
*/
public void addItems(List<T> data, int position) {
if (position >= 0 && position <= mDataList.size() && data != null && data.size() > 0) {
mDataList.addAll(position, data);
notifyItemRangeChanged(position, data.size());
}
}
/**
* 批量添加記錄
*
* @param data 需要加入的數據結構
*/
public void addItems(List<T> data) {
addItems(data, mDataList.size());
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof BaseViewHolder) {
((BaseViewHolder) holder).bindViewData(getItemData(position));
}
}
}
//不是內部類哦
public abstract class BaseViewHolder<T> extends RecyclerView.ViewHolder {
public BaseViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
public abstract void bindViewData(T data);
}
好了,代碼貼完了,應該沒有什么難懂的代碼吧,這一套封裝基本上可以滿足98%以上的單Item 列表了,至于點擊事件,并不是所有的列別都需要,根據實際需要自己定義接口回調就行了。
到這里可能有的同學會問,那多**** Item ****的**** rv ****怎么辦,在實際開發中,多**** Item ****的**** rv ****情景也不算少,特別是聊天列表,動輒八九種**** Item****。別急,我們慢慢來****~****
rv 的方法中有個抽象方法onCreateViewHolder(ViewGroup parent, int viewType)
,這個方法是用來創建 ViewHolder 的,ViewHolder 我們可以把它理解成RecycleView 一個 ItemView 的包裝類,也就是說一個 ViewHolder 就是一個條目,如果我們需要多條目,那么直接在這里返回不同的條目就行了,方法參數里面正好有個 viewType可以用來控制條目類型。
那么問題來了,這個 viewType值是從哪里來的呢,想知道這個,那就只能去看源碼了,我們通過產看 RecyclerView.Adapter 的源碼發現,onCreateViewHolder方法是在createViewHolder里面調用
public final VH createViewHolder(ViewGroup parent, int viewType) {
TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
final VH holder = onCreateViewHolder(parent, viewType);
holder.mItemViewType = viewType;
TraceCompat.endSection();
return holder;
}
看到這里,我們只能繼續追createViewHolder的調用。然后通過全局搜索,在getViewForPosition方法里面找到了viewType這個參數的來源,里面有一行代碼 final int type = mAdapter.getItemViewType(offsetPosition);
于是再繼續追getItemViewType。
public int getItemViewType(int position) {
return 0;
}
好了,追到這里我也不再贅述了,本來大家都知道重寫getItemViewType方法就行了。
回到正題,怎么封裝多 ItemAdapter。多 Item 用到的場景一般都是需要給 RV 添加一個頭或者添加一個尾,因此,考慮到通用性,我就只做了三種類型條目的擴展,下面直接貼代碼:
public abstract class BaseAbstractMultipleItemAdapter<T> extends BaseAbstractAdapter<T> {
private static final int ITEM_TYPE_HEADER = 1;
private static final int ITEM_TYPE_BOTTOM = 2;
private static final int ITEM_TYPE_CONTENT = 3;
@IntDef({ITEM_TYPE_HEADER, ITEM_TYPE_BOTTOM})
@interface ItemType {
}
protected int mHeaderCount;//頭部View個數
protected int mBottomCount;//底部View個數
public BaseAbstractMultipleItemAdapter(Context context) {
super(context);
}
public void setHeaderCount(int headerCount) {
this.mHeaderCount = headerCount;
}
public void setBottomCount(int bottomCount) {
this.mBottomCount = bottomCount;
}
public int getHeaderCount() {
return mHeaderCount;
}
public int getBottomCount() {
return mBottomCount;
}
public boolean isHeaderView(int position) {
return mHeaderCount != 0 && position < mHeaderCount;
}
public boolean isBottomView(int position) {
return mBottomCount != 0 && position >= (mHeaderCount + super.getItemCount());
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ITEM_TYPE_HEADER) {
return onCreateHeaderView(parent);
} else if (viewType == ITEM_TYPE_BOTTOM) {
return onCreateBottomView(parent);
} else {
return onCreateContentView(parent, viewType);
}
}
@Override
public int getItemViewType(int position) {
if (isHeaderView(position)) {//頭部View
return ITEM_TYPE_HEADER;
} else if (isBottomView(position)) {//底部View
return ITEM_TYPE_BOTTOM;
} else {
return getContentViewType(position);
}
}
@Override
public int getItemCount() {
return mHeaderCount + super.getItemCount() + mBottomCount;
}
@Override
public T getItemData(int position) {
int index = position - mHeaderCount;
if (index >= super.getItemCount()) {
return null;
}
return super.getItemData(index);
}
/**
* 移除某一條記錄
*
* @param position 移除數據的position 如果有Header需要減去Header數量
*/
public void removeItem(int position) {
if (position < mDataList.size()) {
mDataList.remove(position);
notifyItemRemoved(mHeaderCount + position);
}
}
/**
* 添加一條記錄
*
* @param data 需要加入的數據結構
* @param position 插入數據的位置 如果有Header需要減去Header數量
*/
public void addItem(T data, int position) {
if (position <= mDataList.size()) {
mDataList.add(position, data);
notifyItemInserted(mHeaderCount + position);
}
}
/**
* 移除所有記錄
*/
public void clearItems() {
int size = mDataList.size();
if (size > 0) {
mDataList.clear();
notifyItemRangeRemoved(mHeaderCount, size);
}
}
/**
* 批量添加記錄
* @param data 需要加入的數據結構
* @param position 插入數據的位置 如果有Header需要減去Header數量
*/
public void addItems(List<T> data, int position) {
if (position <= mDataList.size() && data != null && data.size() > 0) {
mDataList.addAll(position, data);
notifyItemRangeChanged(mHeaderCount + position, data.size());
}
}
public int getContentViewType(int position) {
return ITEM_TYPE_CONTENT;
}
public RecyclerView.ViewHolder onCreateHeaderView(ViewGroup parent) {//創建頭部View
return null;
}
public abstract RecyclerView.ViewHolder onCreateContentView(ViewGroup parent, int viewType);//創建中間內容View
public abstract RecyclerView.ViewHolder onCreateBottomView(ViewGroup parent);//創建底部View
}
代碼都有注釋,應該沒有什么看不懂多看幾遍不能理解的通過mHeaderCount和mBottomCount分別控制頭尾條目數,然后需要什么條目就重寫對應的 CreateViewHolder 方法即可,如果 head 或者 bottom 需要綁定數據就在onBindViewHolder里面根據holder 和 position自行綁定。
三、ItemDecoration
Decoration:n.裝飾品;裝飾,裝潢;裝飾圖案,裝飾風格;獎章
顧名思義,條目裝飾。
我們在寫開發中經常會遇到這個的需求,列表的條目和條目之間需要添加一個間隔線,不知道你們是怎么解決的,反正我之前是直接在條目布局的底部直接寫了一個分割線進去。這樣寫當然也可以,雖然最后一個條目不需要分割線可以通過代碼手動隱藏掉,但是真的很 low有木有,做為一個自命不凡的 Coder,怎么寫出如此高耦合重復的代碼。
ItemDecoration 就可以完美的幫我們解決分割線的問題,當然ItemDecoration的功能可不僅僅如此,一口吃不成胖子,我們一步一步來
使用:mRecycleView.addItemDecoration(new RecyclerView.ItemDecoration() {});
然而這只是一個抽象類。。。
首先我們點進源碼,看這個類的注釋
/**
* An ItemDecoration allows the application to add a special drawing and layout offset
* to specific item views from the adapter's data set. This can be useful for drawing dividers
* between items, highlights, visual grouping boundaries and more.
*
* <p>All ItemDecorations are drawn in the order they were added, before the item
* views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
* and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
* RecyclerView.State)}.</p>
*/
我英語不怎么好,就不一句一句的翻譯了,大意就是:
一個ItemDecoration允許添加一個特殊的圖形和布局偏移。比如說涌入繪制項目之間的分割、突出顯示、視覺分組邊界等等。
所有的 ItemDecoration 的繪制順序和條目添加順序一致,后面這句話我翻譯不通順???♀?。
查看了一下ItemDecoration類的繼承關系,發現Google 給我們默認實現的子類就只有一個ItemTouchHelper,這是一個比較特殊的子類,我們在后面會講。那么既然這樣,我們就只能手擼。
手擼之前我們先看一下ItemDecoration類的結構,在這里提取出三個比較關鍵得方法
public void onDraw(Canvas c, RecyclerView parent, State state)
public void onDrawOver(Canvas c, RecyclerView parent, State state)
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
源碼上面三個方法都有方法說明,但是英語渣的我就不誤導大家的英語了,我直接講方法吧。
-
getItemOffsets()
顧名思義,獲取條目偏移,可以實現類似padding的效果,我這里偷了一張圖便于大家理解:
圖片來源于網絡、不知道哪位好漢的原創 onDraw()
繪制背景,就是繪制的東西會在條目的下層onDrawOver()
繪制覆蓋物,就是繪制的東西會覆蓋在條目上
可能還有點懵逼,但是知道了這三個方法,我們就可以動手給 RecycleView 設置分割線了,下面是一個設置1px 分割線的代碼
public class DividerDecoration extends RecyclerView.ItemDecoration {
private int dividerHeight;
private Paint dividerPaint;
public SimpleDividerDecoration(Context context) {
dividerPaint = new Paint();
dividerPaint.setColor(Color.RED);
dividerHeight = 1
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.bottom = dividerHeight;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
int childCount = parent.getChildCount();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
for (int i = 0; i < childCount - 1; i++) {
View view = parent.getChildAt(i);
float top = view.getBottom();
float bottom = view.getBottom() + dividerHeight;
c.drawRect(left, top, right, bottom, dividerPaint);
}
}
}
注意:這里有個坑getItemOffsets()里面最好不要調用 super 方法,因為里面有一個默認實現outRect.set(0, 0, 0, 0);super 方法最先調用還好,如果在最后調用,我們的outRect參數就被重置了。
四、ItemAnimator
顧名思義,條目動畫。本來不想寫的,RecyclerView 有默認的實現動畫,而且列表中根本用不到酷炫的條目動畫,無奈被我早起規劃的時候,加入了 RecyclerView 的知識點里面,在這里簡單講一下吧。
使用方法如下:
public void setItemAnimator(ItemAnimator animator) {
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
mItemAnimator.setListener(null);
}
mItemAnimator = animator;
if (mItemAnimator != null) {
mItemAnimator.setListener(mItemAnimatorListener);
}
}
從這個方法里面,我們可以 get 到兩個重要信息:
- 自定義條目動畫必須繼承ItemAnimator。
- RecyclerView 有默認的實現動畫 DefaultAnimation,并且在定義變量的時候就賦值給mItemAnimator。
好了,那么接下來我們就通過學習DefaultAnimation的實現來自定義 ItemAnimation。
DefaultAnimation 繼承自SimpleItemAnimator,通過閱讀 SimpleItemAnimator 的注釋信息,我們知道它是RecyclerView.ItemAnimator的直接子類,并且添加了ItemHolderInfo(一個簡單的數據結構,保存了 Item 的邊界信息,用于計算項目動畫)來輔助條目動畫的執行。因此我們如果要自定義 ItemAnimation 最好繼承自SimpleItemAnimator。
好了,到這里,我們算是知道了如何自定義 ItemAnimation,接下來我們只需要順藤摸瓜就可以了。
要順藤摸瓜,得先找到藤,我們去看看ItemAnimation的抽象方法,自定義 ItemAnimation 一共有八個相關的方法需要我們去手動實現
//Item移除回調
@Override
public boolean animateRemove(RecyclerView.ViewHolder holder) {
return false;
}
//Item添加回調
@Override
public boolean animateAdd(RecyclerView.ViewHolder holder) {
return false;
}
//用于控制添加,移動更新時,其它Item的動畫執行
@Override
public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
return false;
}
//Item更新回調
@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
return false;
}
//真正控制執行動畫的地方
@Override
public void runPendingAnimations() {
}
//停止某個Item的動畫
@Override
public void endAnimation(RecyclerView.ViewHolder item) {
}
//停止所有動畫
@Override
public void endAnimations() {
}
@Override
public boolean isRunning() {
return false;
}
沒個方法的作用我都在上面寫了注釋,相信看懂應該不難,接下來我們再根據這根藤回到 DefaultItemAnimation。
- animateAdd開始吧,方法很簡單,就三行代碼。一是清楚和刪除 Item 里面所有的動畫相關代碼,二是把條目初始化為透明狀態(可對比默認執行動畫),三是把條目添加到等待運行動畫列表里面。
@Override
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
mPendingAdditions.add(holder);
return true;
}
- animateRemove,和animateAdd一樣,就少了一行初始化條目的代碼。
@Override
public boolean animateRemove(final ViewHolder holder) {
resetAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
- 再看animateMove()方法,忘記這個方法作用的同學請再回頭看看。這個方法里面有幾個參數,分別是要移動的條目,起始 xy 軸的位置。里面的操作也很簡單,移動Item,然后保存 Item 的移動信息。
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += ViewCompat.getTranslationX(holder.itemView);
fromY += ViewCompat.getTranslationY(holder.itemView);
resetAnimation(holder);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
if (deltaX != 0) {
ViewCompat.setTranslationX(view, -deltaX);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, -deltaY);
}
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
- 接下來看animateChange(),我們看到,如果是同一個 ViewHolder 則直接調用 animateMove()方法,否則在內部多記錄了一個 alpha 的值
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
if (oldHolder == newHolder) {
// Don't know how to run change animations when the same view holder is re-used.
// run a move animation to handle position changes.
return animateMove(oldHolder, fromX, fromY, toX, toY);
}
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
resetAnimation(oldHolder);
int deltaX = (int) (toX - fromX - prevTranslationX);
int deltaY = (int) (toY - fromY - prevTranslationY);
// recover prev translation state after ending animation
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null) {
// carry over translation values
resetAnimation(newHolder);
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
ViewCompat.setAlpha(newHolder.itemView, 0);
}
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
endAnimation()方法和endAnimations()方法一樣,就是循環把待處理的動畫信息全部刪掉,然后調用 cancelAll()停止正在運行的動畫。
isRunning,通過判斷動畫隊列,看是否有動畫待執行動畫或者正在執行的動畫
runPendingAnimations(),真正執行動畫的地方,判斷待執行的動畫隊列里面是否有需要執行的動畫,如果有,就順序執行,如果沒有就退出。
animateAddImpl()、animateChangeImpl()、animateMoveImpl、animateRemoveImpl()這幾個方法分別是幾種類型動畫的具體實現。
好累啊??,終于把它分析完了,接下來我們一鼓作氣,動手擼一個。但是我項目中好像沒有現成的,然后寫動畫這種需要創意的事情我心累,而且關鍵是實際開發中幾乎也不怎么用得到。但是有一部分同學可能會對動畫比較感興趣,于是機智的我去 github 上找了一個RecyclerView 的 Item 動畫庫,大家可以對著我的分析去看看別人的實現,貼上傳送門:https://github.com/wasabeef/recyclerview-animators
五、ItemTouchHelper
ItemTouchHelper 條目觸摸助手,顧名思義,這個類就是用來幫助我們處理 RV 條目觸摸事件的,如常見的滑動刪除,長按拖拽。效果圖如下:
沒有啥特別的,我直接貼代碼吧:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
if (from < to) {
for (int i = from; i < to; i++)
Collections.swap(mNewTopsAdapter.getDataList(), i, i + 1);
} else {
for (int i = from; i > to; i--)
Collections.swap(mNewTopsAdapter.getDataList(), i, i - 1);
}
mNewTopsAdapter.notifyItemMoved(from, to);
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mNewTopsAdapter.removeItem(viewHolder.getAdapterPosition());
}
});
itemTouchHelper.attachToRecyclerView(mRecycleView);
實現起來很簡單,new一個 ItemTouchHelper, 然后調用ItemTouchHelper的 attachToRecyclerView() 依附給 RV 就行了。關鍵點在 ItemTouchHelper 的構造方法里面必傳的參數ItemTouchHelper.Callback()。由于這里需求比較簡單,我直接用了已經做過一次封裝的SimpleCallback。
SimpleCallback:繼承自ItemTouchHelper.Callback,對父類進行了包裝,只暴露出兩個簡單的方法供開發者去實現。
構造方法:
- SimpleCallback(int dragDirs, int swipeDirs)
- dragDirs:條目的拖動方向,可配參數有ItemTouchHelper.UP、ItemTouchHelper.DOWN、ItemTouchHelper.LEFT、ItemTouchHelper.RIGHT,如果要同時配置多個方向,用運算符號|連接
- swipeDirs:條目滑動方向,參數類型同上
我們在 new SimpleCallback()的時候把拖拽的參數配置好,然后再實現如下兩個抽象方法
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)
其中 onMove方法會在 Item 拖動的時候不斷調用,此時我們需要在條目拖動的時候調用 adapter 的notifyItemMoved()方法刷新條目位置,具體代碼如下:
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
if (from < to) {
for (int i = from; i < to; i++)
Collections.swap(mNewTopsAdapter.getDataList(), i, i + 1);
} else {
for (int i = from; i > to; i--)
Collections.swap(mNewTopsAdapter.getDataList(), i, i - 1);
}
mNewTopsAdapter.notifyItemMoved(from, to);
return true;
首先獲取當前條目和目標條目position,這里需要注意getAdapterPosition()和getLayoutPosition(),前者是在adapter 調用界面刷新的時候就給 position 賦值了,而后者是在界面刷新結束之后才能獲取到正確的賦值。我們都知道,RV 的界面刷新是異步的,大概會有一個16毫秒左右的延時,因此使用getLayoutPosition()獲取 position 可能會出錯哦。
onSwiped方法則是用來控制條目滑動刪除之后的邏輯處理。其中direction參數是滑動的方向。比如說滑動刪除:我們直接在方法體里面調用 adapter 的 notifyItemRemoved()方法即可。
好了,ItemTouchHelper 的基本用法就這些,基本也能滿足開發過程中的大部分需求了,如果還有更高的需求,那么繼續跟我去肛一波ItemTouchHelper.Callback的源碼。
****ItemTouchHelper.Callback****
抽象方法有三個:
- public int getMovementFlags(RecyclerView, RecyclerView.ViewHolder)
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)
其中第二個第三個方法,我們在SimpleCallback已經解釋過一次了,這里不在贅述。我們來講一下第一個方法
- public abstract int getMovementFlags(RecyclerView recyclerView,ViewHolder viewHolder);
方法說明上,我用我三級英語水平加上翻譯軟件,大概可以讀出這個方法是要返回一個控制 item 移動方向的混合標志,混合標志怎么生成,可以使用方法makeMovementFlags();好,那么實現getMovementFlags的方法體大概就是 return getMovementFlags(dragFlags,swipeFlags);
而 getMovementFlags 要求我們傳兩個參數,這兩個參數怎么傳呢,我們繼續去追getMovementFlags();
public static int makeMovementFlags(int dragFlags, int swipeFlags) {
return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) |
makeFlag(ACTION_STATE_SWIPE, swipeFlags) | makeFlag(ACTION_STATE_DRAG,
dragFlags);
}
方法說明上,我們可以知道這個方法是用來創建移動 flag 的,說白了就是用來控制 Item 的移動/滑動方向,方法中兩個參數分別是拖動 flag 和滑動 flag。看到這里,我們來回想一下SimpleCallback的構造方法,是不是也要傳這兩個參數,而SimpleCallback不需要實現 getMovementFlags()方法,是不是因為已經幫我們實現了,通過查看源碼驗證了我們的猜想。
然后就是一些公共方法,可重寫定制的:
//是否可以把拖動的ViewHolder拖動到目標ViewHolder之上
@Override
public boolean canDropOver(RecyclerView recyclerView,RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
return true;
}
//獲取拖動
@Override
public RecyclerView.ViewHolder chooseDropTarget(RecyclerView.ViewHolder selected, List<RecyclerView.ViewHolder> dropTargets, int curX, int curY) {
return dropTargets.get(0);
}
//調用時與元素的用戶交互已經結束,也就是它也完成了它的動畫時候
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
}
@Override
public int convertToAbsoluteDirection(int flags, int layoutDirection) {
return super.convertToAbsoluteDirection(flags, layoutDirection);
}
//設置手指離開后ViewHolder的動畫時間
@Override
public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
return super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy);
}
@Override
public int getBoundingBoxMargin() {
return super.getBoundingBoxMargin();
}
//返回值作為用戶視為拖動的距離
@Override
public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) {
return super.getMoveThreshold(viewHolder);
}
//返回值滑動消失的距離,滑動小于這個值不消失,大于消失
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
return super.getSwipeEscapeVelocity(defaultValue);
}
//返回值滑動消失的距離, 這里是相對于RecycleView的寬度,0.5f表示為RecycleView的寬度的一半,取值為0~1f之間
@Override
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
return super.getSwipeThreshold(viewHolder);
}
//返回值作為滑動的流程程度,越小越難滑動,越大越好滑動
@Override
public float getSwipeVelocityThreshold(float defaultValue) {
return 1f;
}
//當用戶拖動一個視圖出界的ItemTouchHelper調用
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll) {
return super.interpolateOutOfBoundsScroll(recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
}
//返回值決定是否有滑動操作
@Override
public boolean isItemViewSwipeEnabled() {
return super.isItemViewSwipeEnabled();
}
//返回值決定是否有拖動操作
@Override
public boolean isLongPressDragEnabled() {
return super.isLongPressDragEnabled();
}
//自定義拖動與滑動交互
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
//自定義拖動與滑動交互
@Override
public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
//當onMove return ture的時候調用
@Override
public void onMoved(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int fromPos, RecyclerView.ViewHolder target, int toPos, int x, int y) {
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y);
}
//當拖動或者滑動的ViewHolder改變時調用
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
}
六、LayoutManger
LayoutManger:布局管理
LayoutManger是 RecyclerView 用來管理子 view 布局的一個組件(另一個組件是 Recycler,負責回收視圖),它主要負責三個事情:
1.布局子視圖
2.在滾動的過程中根據子視圖在布局中所處的位置,決定何時添加子視圖和回收視圖
3.滾動子視圖
其中只有滾動子視圖才需要對子視圖回收或添加,而添加子視圖則必然伴隨著所添加對象的布局處理,在滾動過程中,添加一次子視圖只會影響到被添加對象,原有子視圖的相對位置不會變化。
LayoutManger 是 RecyclerView 的一個抽象內部類,一般我們使用它都是使用它的子類:
- LinearLayoutManager
- GridLayoutManager
- StaggeredGridLayoutManager
這三個類的用法我就不過多的贅述了,相信大家都用過,一般情況下,這三個 LayoutManger 也能夠滿足大家99%的需求了。自定義LayoutManger 是一件比較有難度的工程,而且使用場景很少(反正我是沒碰到過這樣的需求)。但是網上有很多炫酷的自定義 LayoutManger 效果,最經典的當屬防探探的卡片式布局,在網上也看過很多自定義 LayoutManger 的文章,但現在還是半吊子。
感興趣的同學可以看看這個庫,里面有 bolg 鏈接。當然,需要的時候再去看也行。傳送門:https://github.com/mcxtzhang/ZLayoutManager
好了,RecyclerView 到這里就講完了,可能有些地方深度不夠,但是基本能滿足大部分的需求了,如果有問題可以留言提問,后者直接私信我。