RecyclerView用法和原始碼深度解析
阿新 • • 發佈:2018-12-12
目錄介紹
- 1.RecycleView的結構
- 2.Adapter
- 2.1 RecyclerView.Adapter扮演的角色
- 2.2 重寫的方法
- 2.3 notifyDataSetChanged()重新整理資料
- 2.4 資料變更通知之觀察者模式
- a.首先看.notifyDataSetChanged()原始碼
- b.接著檢視.notifyChanged()原始碼
- c.接著檢視setAdapter()原始碼中的setAdapterInternal(adapter, false, true)方法
- d.notify……方法被呼叫,重新整理資料
- 3.ViewHolder
- 3.1 ViewHolder的作用
- 3.2 ViewHolder與複用
- 3.3 ViewHolder簡單封裝
- 4.LayoutManager
- 4.1 作用
- 4.2 LayoutManager樣式
- 4.3 LayoutManager當前有且僅有一個抽象函式
- 4.4 setLayoutManager(LayoutManager layout)原始碼
- 5.ItemDecoration
- 5.1 作用
- 5.2 RecyclerView.ItemDecoration是一個抽象類
- 5.3 addItemDecoration()原始碼分析
- a.首先看addItemDecoration原始碼
- b.接著看下markItemDecorInsetsDirty這個方法
- c.接著看下mRecycler.markItemDecorInsetsDirty();這個方法
- d.回過頭在看看addItemDecoration中requestLayout方法
- e.在 RecyclerView 中搜索 mItemDecorations 集合
- 6.ItemAnimator
- 6.1 作用
- 6.2 觸發的三種事件
- 7.其他知識點
- 7.1 Recycler && RecycledViewPool
- 7.2 Recyclerview.getLayoutPosition()區別
- 8.RecyclerView巢狀方案滑動衝突解決方案
- 8.1 如何判斷RecyclerView控制元件滑動到頂部和底部
- 8.2 RecyclerView巢狀RecyclerView 條目自動上滾的Bug
- 8.3 ScrollView巢狀RecyclerView滑動衝突
- 8.4 ViewPager巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager
- 9.RecyclerView複雜佈局封裝庫案例
- 9.1 能夠實現業務的需求和功能
- 9.2 具備的優勢分析
- 10.針對阿里VLayout程式碼分析
- 11.版本更新說明
- v1.0.0 2016年5月5日
- v1.1.0 更新於2017年2月1日
- v1.1.1 更新於2017年6月9日
- v2.0.0 更新於2018年8月21日
- v2.1.0 更新於2018年9月29日
1.RecycleView的結構
- 關於RecyclerView,大家都已經很熟悉了,用途十分廣泛,大概結構如下所示
- RecyclerView.Adapter - 處理資料集合並負責繫結檢視
- ViewHolder - 持有所有的用於繫結資料或者需要操作的View
- LayoutManager - 負責擺放檢視等相關操作
- ItemDecoration - 負責繪製Item附近的分割線
- ItemAnimator - 為Item的一般操作新增動畫效果,如,增刪條目等
- 如圖所示,直觀展示結構
- 針對上面幾個屬性,最簡單用法如下所示
recyclerView = (RecyclerView) findViewById(R.id.recyclerView); LinearLayoutManager layoutManager = new LinearLayoutManager(this); //設定layoutManager recyclerView.setLayoutManager(layoutManager); final RecycleViewItemLine line = new RecycleViewItemLine(this, LinearLayout.HORIZONTAL,1,this.getResources().getColor(R.color.colorAccent)); //設定新增分割線 recyclerView.addItemDecoration(line); adapter = new MultipleItemAdapter(this); //設定adapter recyclerView.setAdapter(adapter); //新增資料並且重新整理adapter adapter.addAll(list); adapter.notifyDataSetChanged(); //adapter //onCreateViewHolder(ViewGroup parent, int viewType)這裡的第二個引數就是View的型別,可以根據這個型別判斷去建立不同item的ViewHolder public class MultipleItemAdapter extends RecyclerView.Adapter<recyclerview.viewholder> { public static enum ITEM_TYPE { ITEM_TYPE_IMAGE, ITEM_TYPE_TEXT } private final LayoutInflater mLayoutInflater; private final Context mContext; private ArrayList<String> mTitles; public MultipleItemAdapter(Context context) { mContext = context; mLayoutInflater = LayoutInflater.from(context); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal()) { return new ImageViewHolder(mLayoutInflater.inflate(R.layout.item_image, parent, false)); } else { return new TextViewHolder(mLayoutInflater.inflate(R.layout.item_text, parent, false)); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (holder instanceof TextViewHolder) { ((TextViewHolder) holder).mTextView.setText(mTitles[position]); } else if (holder instanceof ImageViewHolder) { ((ImageViewHolder) holder).mTextView.setText(mTitles[position]); } } @Override public int getItemViewType(int position) { return position % 2 == 0 ? ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal() : ITEM_TYPE.ITEM_TYPE_TEXT.ordinal(); } @Override public int getItemCount() { return mTitles == null ? 0 : mTitles.length; } public void addAll(ArrayList<String> list){ if(mTitles!=null){ mTitles.clear(); }else { mTitles = new ArrayList<>(); } mTitles.addAll(list); } public static class TextViewHolder extends RecyclerView.ViewHolder { @InjectView(R.id.text_view) TextView mTextView; TextViewHolder(View view) { super(view); ButterKnife.inject(this, view); } } public static class ImageViewHolder extends RecyclerView.ViewHolder { @InjectView(R.id.text_view) TextView mTextView; @InjectView(R.id.image_view) ImageView mImageView; ImageViewHolder(View view) { super(view); ButterKnife.inject(this, view); } } }
2.Adapter
2.1 RecyclerView.Adapter扮演的角色
- 一是,根據不同ViewType建立與之相應的的Item-Layout
- 二是,訪問資料集合並將資料繫結到正確的View上
2.2 重寫的方法
- 一般常用的重寫方法有以下這麼幾個:
public VH onCreateViewHolder(ViewGroup parent, int viewType) 建立Item檢視,並返回相應的ViewHolder public void onBindViewHolder(VH holder, int position) 繫結資料到正確的Item檢視上。 public int getItemCount() 返回該Adapter所持有的Itme數量 public int getItemViewType(int position) 用來獲取當前項Item(position引數)是哪種型別的佈局
2.3 notifyDataSetChanged()重新整理資料
- 當時據集合發生改變時,我們通過呼叫.notifyDataSetChanged(),來重新整理列表,因為這樣做會觸發列表的重繪,所以並不會出現任何動畫效果,因此需要呼叫一些以notifyItem*()作為字首的特殊方法,比如:
- public final void notifyItemInserted(int position) 向指定位置插入Item
- public final void notifyItemRemoved(int position) 移除指定位置Item
- public final void notifyItemChanged(int position) 更新指定位置Item
2.4 資料變更通知之觀察者模式
- a.首先看.notifyDataSetChanged()原始碼
/** @see #notifyItemChanged(int) * @see #notifyItemInserted(int) * @see #notifyItemRemoved(int) * @see #notifyItemRangeChanged(int, int) * @see #notifyItemRangeInserted(int, int) * @see #notifyItemRangeRemoved(int, int) */ public final void notifyDataSetChanged() { mObservable.notifyChanged(); }
- b.接著檢視.notifyChanged();原始碼
- 被觀察者AdapterDataObservable,內部持有觀察者AdapterDataObserver集合
static class AdapterDataObservable extends Observable<AdapterDataObserver> { public boolean hasObservers() { return !mObservers.isEmpty(); } public void notifyChanged() { for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onChanged(); } } public void notifyItemRangeChanged(int positionStart, int itemCount) { notifyItemRangeChanged(positionStart, itemCount, null); } public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) { for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload); } } public void notifyItemRangeInserted(int positionStart, int itemCount) { for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onItemRangeInserted(positionStart, itemCount); } } }
- 觀察者AdapterDataObserver,具體實現為RecyclerViewDataObserver,當資料來源發生變更時,及時響應介面變化
public static abstract class AdapterDataObserver { public void onChanged() { // Do nothing } public void onItemRangeChanged(int positionStart, int itemCount) { // do nothing } public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { onItemRangeChanged(positionStart, itemCount); } }
- c.接著檢視setAdapter()原始碼中的setAdapterInternal(adapter, false, true)方法
- setAdapter原始碼
public void setAdapter(Adapter adapter) { // bail out if layout is frozen setLayoutFrozen(false); setAdapterInternal(adapter, false, true); requestLayout(); }
- setAdapterInternal(adapter, false, true)原始碼
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) { if (mAdapter != null) { mAdapter.unregisterAdapterDataObserver(mObserver); mAdapter.onDetachedFromRecyclerView(this); } if (!compatibleWithPrevious || removeAndRecycleViews) { removeAndRecycleViews(); } mAdapterHelper.reset(); final Adapter oldAdapter = mAdapter; mAdapter = adapter; if (adapter != null) { //註冊一個觀察者RecyclerViewDataObserver adapter.registerAdapterDataObserver(mObserver); adapter.onAttachedToRecyclerView(this); } if (mLayout != null) { mLayout.onAdapterChanged(oldAdapter, mAdapter); } mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious); mState.mStructureChanged = true; markKnownViewsInvalid(); }
- d.notify……方法被呼叫,重新整理資料
- 當資料變更時,呼叫notify**方法時,Adapter內部的被觀察者會遍歷通知已經註冊的觀察者的對應方法,這時介面就會響應變更。
3.ViewHolder
3.1 ViewHolder的作用
- ViewHolder作用大概有這些:
- adapter應當擁有ViewHolder的子類,並且ViewHolder內部應當儲存一些子view,避免時間代價很大的findViewById操作
- 其RecyclerView內部定義的ViewHolder類包含很多複雜的屬性,內部使用場景也有很多,而我們經常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一個新型別。item的ViewHolder時呼叫來建立一個ViewHolder,而onBindViewHolder()方法則當RecyclerView需要在特定位置的item展示資料時呼叫。
3.2 ViewHolder與複用
- 在複寫RecyclerView.Adapter的時候,需要我們複寫兩個方法:
- onCreateViewHolder
- onBindViewHolder
- 這兩個方法從字面上看就是建立ViewHolder和繫結ViewHolder的意思
- 複用機制是怎樣的?
- 模擬場景:只有一種ViewType,上下滑動的時候需要的ViewHolder種類是隻有一種,但是需要的ViewHolder物件數量並不止一個。所以在後面建立了5個ViewHolder之後,需要的數量夠了,無論怎麼滑動,都只需要複用以前建立的物件就行了。那麼逗比程式設計師們思考一下,為什麼會出現這種情況呢
- 看到了下面log之後,第一反應是在這個ViewHolder物件的數量“夠用”之後就停止呼叫onCreateViewHolder方法,但是onBindViewHolder方法每次都會呼叫的
- 檢視一下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; }
- 對於ViewHolder物件的數量“夠用”之後就停止呼叫onCreateViewHolder方法,可以檢視
- 獲取為給定位置初始化的檢視。
- 此方法應由{@link LayoutManager}實現使用,以獲取檢視來表示來自{@LinkAdapter}的資料。
- 如果共享池可用於正確的檢視型別,則回收程式可以重用共享池中的廢檢視或分離檢視。如果介面卡沒有指示給定位置上的資料已更改,則回收程式將嘗試發回一個以前為該資料初始化的報廢檢視,而不進行重新繫結。
public View getViewForPosition(int position) { return getViewForPosition(position, false); } View getViewForPosition(int position, boolean dryRun) { return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; } @Nullable ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) { //程式碼省略了,有需要的小夥伴可以自己看看,這裡面邏輯實在太複雜呢 }
3.3 ViewHolder簡單封裝
- 關於ViewHolder簡單的封裝程式碼如下所示:
public abstract class BaseMViewHolder<M> extends RecyclerView.ViewHolder { // SparseArray 比 HashMap 更省記憶體,在某些條件下效能更好,只能儲存 key 為 int 型別的資料, // 用來存放 View 以減少 findViewById 的次數 private SparseArray<View> viewSparseArray; BaseMViewHolder(View itemView) { super(itemView); if(viewSparseArray==null){ viewSparseArray = new SparseArray<>(); } } public BaseMViewHolder(ViewGroup parent, @LayoutRes int res) { super(LayoutInflater.from(parent.getContext()).inflate(res, parent, false)); if(viewSparseArray==null){ viewSparseArray = new SparseArray<>(); } } /** * 子類設定資料方法 * @param data */ public void setData(M data) {} /** * 第二種findViewById方式 * 根據 ID 來獲取 View * @param viewId viewID * @param <T> 泛型 * @return 將結果強轉為 View 或 View 的子型別 */ @SuppressWarnings("unchecked") protected <T extends View> T getView(int viewId) { // 先從快取中找,找打的話則直接返回 // 如果找不到則 findViewById ,再把結果存入快取中 View view = viewSparseArray.get(viewId); if (view == null) { view = itemView.findViewById(viewId); viewSparseArray.put(viewId, view); } return (T) view; } /** * 獲取上下文context * @return context */ protected Context getContext(){ return itemView.getContext(); } /** * 獲取資料索引的位置 * @return position */ protected int getDataPosition(){ RecyclerView.Adapter adapter = getOwnerAdapter(); if (adapter!=null && adapter instanceof RecyclerArrayAdapter){ return getAdapterPosition() - ((RecyclerArrayAdapter) adapter).getHeaderCount(); } return getAdapterPosition(); } /** * 獲取adapter物件 * @param <T> * @return adapter */ @Nullable private <T extends RecyclerView.Adapter> T getOwnerAdapter(){ RecyclerView recyclerView = getOwnerRecyclerView(); //noinspection unchecked return recyclerView != null ? (T) recyclerView.getAdapter() : null; } @Nullable private RecyclerView getOwnerRecyclerView(){ try { Field field = RecyclerView.ViewHolder.class.getDeclaredField("mOwnerRecyclerView"); field.setAccessible(true); return (RecyclerView) field.get(this); } catch (NoSuchFieldException ignored) { ignored.printStackTrace(); } catch (IllegalAccessException ignored) { ignored.printStackTrace(); } return null; } /** * 新增子控制元件的點選事件 * @param viewId 控制元件id */ protected void addOnClickListener(@IdRes final int viewId) { final View view = getView(viewId); if (view != null) { if (!view.isClickable()) { view.setClickable(true); } view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(getOwnerAdapter()!=null){ if (((RecyclerArrayAdapter)getOwnerAdapter()).getOnItemChildClickListener() != null) { ((RecyclerArrayAdapter)getOwnerAdapter()).getOnItemChildClickListener() .onItemChildClick(v, getDataPosition()); } } } }); } } //省略部分程式碼 //關於adapter封裝可以檢視我的開源adpater封裝庫:https://github.com/yangchong211/YCBaseAdapter //關於recyclerView封裝庫,可以檢視我的開源庫:https://github.com/yangchong211/YCRefreshView }
4.LayoutManager
4.1 作用
- LayoutManager的職責是擺放Item的位置,並且負責決定何時回收和重用Item。
- RecyclerView 允許自定義規則去放置子 view,這個規則的控制者就是 LayoutManager。一個 RecyclerView 如果想展示內容,就必須設定一個 LayoutManager
4.2 LayoutManager樣式
- LinearLayoutManager 水平或者垂直的Item檢視。
- GridLayoutManager 網格Item檢視。
- StaggeredGridLayoutManager 交錯的網格Item檢視。
4.3 LayoutManager當前有且僅有一個抽象函式
- 具體如下:
public LayoutParams generateDefaultLayoutParams()
4.4 setLayoutManager(LayoutManager layout)原始碼
- a.setLayoutManager入口原始碼
- 分析:當之前設定過 LayoutManager 時,移除之前的檢視,並快取檢視在 Recycler 中,將新的 mLayout 物件與 RecyclerView 繫結,更新快取 View 的數量。最後去呼叫 requestLayout ,重新請求 measure、layout、draw。
public void setLayoutManager(LayoutManager layout) { if (layout == mLayout) { return; } // 停止滑動 stopScroll(); if (mLayout != null) { // 如果有動畫,則停止所有的動畫 if (mItemAnimator != null) { mItemAnimator.endAnimations(); } // 移除並回收檢視 mLayout.removeAndRecycleAllViews(mRecycler); // 回收廢棄檢視 mLayout.removeAndRecycleScrapInt(mRecycler); //清除mRecycler mRecycler.clear(); if (mIsAttached) { mLayout.dispatchDetachedFromWindow(this, mRecycler); } mLayout.setRecyclerView(null); mLayout = null; } else { mRecycler.clear(); } mChildHelper.removeAllViewsUnfiltered(); mLayout = layout; if (layout != null) { if (layout.mRecyclerView != null) { throw new IllegalArgumentException("LayoutManager " + layout + " is already attached to a RecyclerView: " + layout.mRecyclerView); } mLayout.setRecyclerView(this); if (mIsAttached) { mLayout.dispatchAttachedToWindow(this); } } //更新新的快取資料 mRecycler.updateViewCacheSize(); //重新請求 View 的測量、佈局、繪製 requestLayout(); }
5.ItemDecoration
5.1 作用
- 通過設定recyclerView.addItemDecoration(new DividerDecoration(this));來改變Item之間的偏移量或者對Item進行裝飾。
- 當然,你也可以對RecyclerView設定多個ItemDecoration,列表展示的時候會遍歷所有的ItemDecoration並呼叫裡面的繪製方法,對Item進行裝飾。
5.2 RecyclerView.ItemDecoration是一個抽象類
- 該抽象類常見的方法如下所示:
public void onDraw(Canvas c, RecyclerView parent) 裝飾的繪製在Item條目繪製之前呼叫,所以這有可能被Item的內容所遮擋 public void onDrawOver(Canvas c, RecyclerView parent) 裝飾的繪製在Item條目繪製之後呼叫,因此裝飾將浮於Item之上 public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) 與padding或margin類似,LayoutManager在測量階段會呼叫該方法,計算出每一個Item的正確尺寸並設定偏移量。
5.3 .addItemDecoration()原始碼分析
- a.通過下面程式碼可知,mItemDecorations是一個ArrayList,我們將ItemDecoration也就是分割線物件,新增到其中。
- 可以看到,當通過這個方法新增分割線後,會指定新增分割線在集合中的索引,然後再重新請求 View 的測量、佈局、(繪製)。注意: requestLayout會呼叫onMeasure和onLayout,不一定呼叫onDraw!
public void addItemDecoration(ItemDecoration decor) { addItemDecoration(decor, -1); } //主要看這個方法,我的GitHub:https://github.com/yangchong211/YCBlogs public void addItemDecoration(ItemDecoration decor, int index) { if (mLayout != null) { mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or" + " layout"); } if (mItemDecorations.isEmpty()) { setWillNotDraw(false); } if (index < 0) { mItemDecorations.add(decor); } else { // 指定新增分割線在集合中的索引 mItemDecorations.add(index, decor); } markItemDecorInsetsDirty(); // 重新請求 View 的測量、佈局、繪製 requestLayout(); }
- b.接著看下markItemDecorInsetsDirty這個方法做了些什麼
- 這個方法先獲取所有子View的數量,然後遍歷了 RecyclerView 和 LayoutManager 的所有子 View,再將其子 View 的 LayoutParams 中的 mInsetsDirty 屬性置為 true,最後呼叫了 mRecycler.markItemDecorInsetsDirty()方法處理複用邏輯。
void markItemDecorInsetsDirty() { final int childCount = mChildHelper.getUnfilteredChildCount(); //先遍歷了 RecyclerView 和 LayoutManager 的所有子 View for (int i = 0; i < childCount; i++) { final View child = mChildHelper.getUnfilteredChildAt(i); //將其子 View 的 LayoutParams 中的 mInsetsDirty 屬性置為 true ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; } //呼叫了 mRecycler.markItemDecorInsetsDirty(), //Recycler 是 RecyclerView 的一個內部類,就是它管理著 RecyclerView 的複用邏輯 mRecycler.markItemDecorInsetsDirty(); }
- c.接著看下markItemDecorInsetsDirty()這個方法
- 該方法就是獲取RecyclerView 快取的集合,然後遍歷集合得到RecyclerView 的快取單位是 ViewHolder,獲取快取物件,在獲取到layoutParams,並且將其 mInsetsDirty 欄位一樣置為 true
void markItemDecorInsetsDirty() { //就是 RecyclerView 快取的集合 final int cachedCount = mCachedViews.size(); for (int i = 0; i < cachedCount; i++) { //RecyclerView 的快取單位是 ViewHolder,獲取快取物件 final ViewHolder holder = mCachedViews.get(i); //獲得 LayoutParams LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams(); if (layoutParams != null) { //將其 mInsetsDirty 欄位一樣置為 true layoutParams.mInsetsDirty = true; } } }
- d.回過頭在看看addItemDecoration中requestLayout方法
- requestLayout 方法用一種責任鏈的方式,層層向上傳遞,最後傳遞到 ViewRootImpl,然後重新呼叫 view 的 measure、layout、draw 方法來展示佈局
@CallSuper public void requestLayout() { if (mMeasureCache != null) mMeasureCache.clear(); if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) { // Only trigger request-during-layout logic if this is the view requesting it, // not the views in its parent hierarchy ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null && viewRoot.isInLayout()) { if (!viewRoot.requestLayoutDuringLayout(this)) { return; } } mAttachInfo.mViewRequestingLayout = this; } mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); } if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) { mAttachInfo.mViewRequestingLayout = null; } }
- e.在 RecyclerView 中搜索 mItemDecorations 集合
- 在onDraw中
@Override public void onDraw(Canvas c) { super.onDraw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } }
- 在draw方法中
@Override public void draw(Canvas c) { super.draw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDrawOver(c, this, mState); } //省略部分程式碼 }
- 總結概括
- 可以看到在 View 的以上兩個方法中,分別呼叫了 ItemDecoration 物件的 onDraw onDrawOver 方法。
- 這兩個抽象方法,由我們繼承 ItemDecoration 來自己實現,他們區別就是 onDraw 在 item view 繪製之前呼叫,onDrawOver 在 item view 繪製之後呼叫。
- 所以繪製順序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。
6.ItemAnimator
6.1 作用
- ItemAnimator能夠幫助Item實現獨立的動畫
6.2 觸發的三種事件
- 某條資料被插入到資料集合中
- 從資料集合中移除某條資料
- 更改資料集合中的某條資料
7.其他知識點
7.1 Recycler && RecycledViewPool
- RecycledViewPool
- RecyclerViewPool用於多個RecyclerView之間共享View。只需要建立一個RecyclerViewPool例項,然後呼叫RecyclerView的setRecycledViewPool(RecycledViewPool)方法即可。RecyclerView預設會建立一個RecyclerViewPool例項。
- 下列原始碼,是我藉助於有道詞典翻譯部分註釋內容……
- 看出mScrap是一個<viewType, List>的對映,mMaxScrap是一個<viewType, maxNum>的對映,這兩個成員變數代表可複用View池的基本資訊。呼叫setMaxRecycledViews(int viewType, int max)時,當用於複用的mScrap中viewType對應的ViewHolder個數超過maxNum時,會從列表末尾開始丟棄超過的部分。呼叫getRecycledView(int viewType)方法時從mScrap中移除並返回viewType對應的List的末尾項
public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; static class ScrapData { final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); int mMaxScrap = DEFAULT_MAX_SCRAP; long mCreateRunningAverageNs = 0; long mBindRunningAverageNs = 0; } SparseArray<ScrapData> mScrap = new SparseArray<>(); private int mAttachCount = 0; //丟棄所有檢視 public void clear() { for (int i = 0; i < mScrap.size(); i++) { ScrapData data = mScrap.valueAt(i); data.mScrapHeap.clear(); } } //設定丟棄前要在池中持有的檢視持有人的最大數量 public void setMaxRecycledViews(int viewType, int max) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mMaxScrap = max; final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; while (scrapHeap.size() > max) { scrapHeap.remove(scrapHeap.size() - 1); } } //返回給定檢視型別的RecycledViewPool所持有的當前檢視數 public int getRecycledViewCount(int viewType) { return getScrapDataForType(viewType).mScrapHeap.size(); } //從池中獲取指定型別的ViewHolder,如果沒有指定型別的ViewHolder,則獲取{@Codenull} @Nullable public ViewHolder getRecycledView(int viewType) { final ScrapData scrapData = mScrap.get(viewType); if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; return scrapHeap.remove(scrapHeap.size() - 1); } return null; } //池持有的檢視持有者總數 int size() { int count = 0; for (int i = 0; i < mScrap.size(); i++) { ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i).mScrapHeap; if (viewHolders != null) { count += viewHolders.size(); } } return count; } //向池中新增一個廢檢視儲存器。 //如果那個ViewHolder型別的池已經滿了,它將立即被丟棄。 public void putRecycledView(ViewHolder scrap) { final int viewType = scrap.getItemViewType(); final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap; if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { return; } if (DEBUG && scrapHeap.contains(scrap)) { throw new IllegalArgumentException("this scrap item already exists"); } scrap.resetInternal(); scrapHeap.add(scrap); } long runningAverage(long oldAverage, long newValue) { if (oldAverage == 0) { return newValue; } return (oldAverage / 4 * 3) + (newValue / 4); } void factorInCreateTime(int viewType, long createTimeNs) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mCreateRunningAverageNs = runningAverage( scrapData.mCreateRunningAverageNs, createTimeNs); } void factorInBindTime(int viewType, long bindTimeNs) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mBindRunningAverageNs = runningAverage( scrapData.mBindRunningAverageNs, bindTimeNs); } boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) { long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs; return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); } boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) { long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs; return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); } void attach(Adapter adapter) { mAttachCount++; } void detach() { mAttachCount--; } //分離舊介面卡並附加新介面卡。如果它只附加了一個介面卡,並且新介面卡使用與oldAdapter不同的ViewHolder, //則RecycledViewPool將清除其快取。 void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,boolean compatibleWithPrevious) { if (oldAdapter != null) { detach(); } if (!compatibleWithPrevious && mAttachCount == 0) { clear(); } if (newAdapter != null) { attach(newAdapter); } } private ScrapData getScrapDataForType(int viewType) { ScrapData scrapData = mScrap.get(viewType); if (scrapData == null) { scrapData = new ScrapData(); mScrap.put(viewType, scrapData); } return scrapData; } }
- ViewCacheExtension
- ViewCacheExtension是一個由開發者控制的可以作為View快取的幫助類。呼叫Recycler.getViewForPosition(int)方法獲取View時,Recycler先檢查attachedscrap和一級快取,如果沒有則檢查ViewCacheExtension.getViewForPositionAndType(Recycler, int, int),如果沒有則檢查RecyclerViewPool。注意:Recycler不會在這個類中做快取View的操作,是否快取View完全由開發者控制。
public abstract static class ViewCacheExtension { abstract public View getViewForPositionAndType(Recycler recycler, int position, int type); }
- Recycler
- 後續再深入分析
7.2 Recyclerview.getLayoutPosition()問題
- 在RecycleView中的相關方法中,有兩種型別的位置
- 佈局位置:從LayoutManager的角度看,條目在最新佈局計算中的位置。
- 返回佈局位置的方法使用最近一次佈局運算後的位置,如getLayoutPosition()和findViewHolderForLayoutPosition(int)。這些位置包含了最近一次佈局運算後的變化。你可以根據這些位置來與使用者正在螢幕上看到的保持一致。比如,你有一個條目列表,當用戶請求第5個條目時,你可以使用這些方法來匹配使用者看到的。
- 介面卡位置:從介面卡的角度看,條目在是介面卡中的位置。
- 另外一系列方法與AdapterPosition關聯,比如getAdapterPosition()和findViewHolderForAdapterPosition(int)。當你想獲得條目在更新後的介面卡中的位置使用這些方法,即使這些位置變化還沒反映到佈局中。比如,你想訪問介面卡中條目的位置時,就應該使用getAdapterPosition()。注意,notifyDataSetChanged()已經被呼叫而且還沒計算新佈局,這些方法或許不能夠計算介面卡位置。所以,你要小心處理這些方法返回NO_POSITION和null的情況。
- 注意: 這兩種型別的位置是等同的,除非在分發adapter.notify*事件和更新佈局時。
- 佈局位置:從LayoutManager的角度看,條目在最新佈局計算中的位置。
- 關於兩者的區別
- 網上查了一些資料,發現相關內容很少,最後在stackoverflow上終於看到有大神這樣解釋兩者的區別
- 具體區別就是adapter和layout的位置會有時間差(<16ms), 如果你改變了Adapter的資料然後重新整理檢視, layout需要過一段時間才會更新檢視, 在這段時間裡面, 這兩個方法返回的position會不一樣。
- 在notifyDataSetChanged之後並不能馬上獲取Adapter中的position, 要等佈局結束之後才能獲取到
- 在notifyItemInserted之後,Layout不能馬上獲取到新的position,因為佈局還沒更新(需要<16ms的時間重新整理檢視), 所以只能獲取到舊的,但是Adapter中的position就可以馬上獲取到最新的position。
public final int getAdapterPosition() { if (mOwnerRecyclerView == null) { return NO_POSITION; } return mOwnerRecyclerView.getAdapterPositionFor(this); } public final int getLayoutPosition() { return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; }
- 可能會導致的錯誤
- 這種情況有點難以復現,在 ViewHolder 中處理 item 的點選事件的時候,發現多個 item 同時點選就會出現閃退,debug 看到 position = -1
- 解決辦法:使用 ViewHolder#getLayoutPosition() 獲取 position,而不要通過 ViewHolder#getAdapterPosition() 來獲取 position 的
8.RecyclerView巢狀方案滑動衝突解決方案
8.1 如何判斷RecyclerView控制元件滑動到頂部和底部
- 有一種使用場景,購物商城的購物車頁面,當RecyclerView滑動到頂部時,讓重新整理控制元件消費事件;當RecyclerView滑動到底部時,讓下一頁控制元件[猜你喜歡]消費事件。
- 程式碼如下所示:
public class VerticalRecyclerView extends RecyclerView { private float downX; private float downY; /** 第一個可見的item的位置 */ private int firstVisibleItemPosition; /** 第一個的位置 */ private int[] firstPositions; /** 最後一個可見的item的位置 */ private int lastVisibleItemPosition; /** 最後一個的位置 */ private int[] lastPositions; private boolean isTop; private boolean isBottom; public VerticalRecyclerView(Context context) { this(context, null); } public VerticalRecyclerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public VerticalRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager != null) { if (layoutManager instanceof GridLayoutManager) { lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); firstVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition(); } else if (layoutManager instanceof LinearLayoutManager) { lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; if (lastPositions == null) { lastPositions = new int[staggeredGridLayoutManager.getSpanCount()]; firstPositions = new int[staggeredGridLayoutManager.getSpanCount()]; } staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions); staggeredGridLayoutManager.findFirstVisibleItemPositions(firstPositions); lastVisibleItemPosition = findMax(lastPositions); firstVisibleItemPosition = findMin(firstPositions); } } else { throw new RuntimeException("Unsupported LayoutManager used. Valid ones are LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager"); } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: downX = ev.getX(); downY = ev.getY(); //如果滑動到了最底部,就允許繼續向上滑動載入下一頁,否者不允許 getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: float dx = ev.getX() - downX; float dy = ev.getY() - downY; boolean allowParentTouchEvent; if (Math.abs(dy) > Math.abs(dx)) { if (dy > 0) { //位於頂部時下拉,讓父View消費事件 allowParentTouchEvent = isTop = firstVisibleItemPosition == 0 && getChildAt(0).getTop() >= 0; } else { //位於底部時上拉,讓父View消費事件 int visibleItemCount = layoutManager.getChildCount(); int totalItemCount = layoutManager.getItemCount(); allowParentTouchEvent = isBottom = visibleItemCount > 0 && (lastVisibleItemPosition) >= totalItemCount - 1 && getChildAt(getChildCount() - 1).getBottom() <= getHeight(); } } else { //水平方向滑動 allowParentTouchEvent = true; } getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent); } return super.dispatchTouchEvent(ev); } private int findMax(int[] lastPositions) { int max = lastPositions[0]; for (int value : lastPositions) { if (value >= max) { max = value; } } return max; } private int findMin(int[] firstPositions) { int min = firstPositions[0]; for (int value : firstPositions) { if (value < min) { min = value; } } return min; } public boolean isTop() { return isTop; } public boolean isBottom() { return isBottom; } }
8.2 RecyclerView巢狀RecyclerView條目自動上滾的Bug
- RecyclerViewA巢狀RecyclerViewB 進入頁面自動跳轉到RecyclerViewB上面頁面會自動滾動。
- 兩種解決辦法
- 一,recyclerview去除焦點
- recyclerview.setFocusableInTouchMode(false);
- recyclerview.requestFocus();
- 二,在程式碼裡面 讓處於ScrollView或者RecyclerView1 頂端的某個控制元件獲得焦點即可
- 比如頂部的一個textview
- tv.setFocusableInTouchMode(true);
- tv.requestFocus();
8.3 ScrollView巢狀RecyclerView滑動衝突
- 第一種方式:
- 重寫父控制元件,讓父控制元件 ScrollView 直接攔截滑動事件,不向下分發給 RecyclerView,具體是定義一個ScrollView子類,重寫其 onInterceptTouchEvent()方法
public class NoNestedScrollview extends NestedScrollView { private int downX; private int downY; private int mTouchSlop; public NoNestedScrollview(Context context) { super(context); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } public NoNestedScrollview(Context context, AttributeSet attrs) { super(context, attrs); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } public NoNestedScrollview(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } @Override public boolean onInterceptTouchEvent(MotionEvent e) { int action = e.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: downX = (int) e.getRawX(); downY = (int) e.getRawY(); break; case MotionEvent.ACTION_MOVE: //判斷是否滑動,若滑動就攔截事件 int moveY = (int) e.getRawY(); if (Math.abs(moveY - downY) > mTouchSlop) { return true; } break; default: break; } return super.onInterceptTouchEvent(e); } }
- 第二種解決方式
- a.禁止RecyclerView滑動
recyclerView.setLayoutManager(new GridLayoutManager(mContext,2){ @Override public boolean canScrollVertically() { return false; } @Override public boolean canScrollHorizontally() { return super.canScrollHorizontally(); } }); recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayout.VERTICAL,false){ @Override public boolean canScrollVertically() { return false; } });
- b.重寫LayoutManager
- 程式碼設定LayoutManager.setScrollEnabled(false);
public class ScrollLayoutManager extends LinearLayoutManager { private boolean isScrollEnable = true; public ScrollLayoutManager(Context context) { super(context); } public ScrollLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); } public ScrollLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public boolean canScrollVertically() { return isScrollEnable && super.canScrollVertically(); } /** * 設定 RecyclerView 是否可以垂直滑動 * @param isEnable */ public void setScrollEnable(boolean isEnable) { this.isScrollEnable = isEnable; } }
- 可能會出現的問題
- 雖然上面兩種方式解決了滑動衝突,但是有的手機上出現了RecyclerView會出現顯示不全的情況。
- 針對這種情形,使用網上的方法一種是使用 RelativeLayout 包裹 RecyclerView 並設定屬性:android:descendantFocusability=“blocksDescendants”
- android:descendantFocusability=“blocksDescendants”,該屬>性是當一個view 獲取焦點時,定義 ViewGroup 和其子控制元件直接的關係,常用來>解決父控制元件的焦點或者點選事件被子空間獲取。
- beforeDescendants: ViewGroup會優先其子控制元件獲取焦點
- afterDescendants: ViewGroup只有當其子控制元件不需要獲取焦點時才獲取焦點
- blocksDescendants: ViewGroup會覆蓋子類控制元件而直接獲得焦點
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:descendantFocusability="blocksDescendants"> <android.support.v7.widget.RecyclerView android:id="@+id/rv_hot_review" android:layout_width="match_parent" android:layout_height="wrap_content" android:foregroundGravity="center" /> </RelativeLayout>
8.4 viewPager巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager
- 繼承RecyclerView,重寫dispatchTouchEvent,根據ACTION_MOVE的方向判斷是否呼叫getParent().requestDisallowInterceptTouchEvent去阻止父view攔截點選事件
@Override public boolean dispatchTouchEvent(MotionEvent ev) { /*---解決垂ViewPager巢狀直RecyclerView巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager start ---*/ ViewParent parent = this; while(!((parent = parent.getParent()) instanceof ViewPager)); // 迴圈查詢viewPager parent.requestDisallowInterceptTouchEvent(true); return super.dispatchTouchEvent(ev); }
9.RecyclerView複雜佈局封裝庫案例
9.1 能夠實現業務的需求和功能
- 1.1 支援上拉載入,下拉重新整理,可以自定義foot底部佈局,支援新增多個自定義header頭部佈局。
- 1.2 支援切換不同的狀態,比如載入中[目前是ProgressBar,載入成功,載入失敗,載入錯誤等不同佈局狀態。當然也可以自定義這些狀態的佈局
- 1.3 支援複雜介面使用,比如有的頁面包含有輪播圖,按鈕組合,橫向滑動,還有複雜list,那麼用這個控制元件就可以搞定。
- 1.4 已經用於實際開發專案投資界,新芽,沙丘大學中……
- 1.5 輕量級側滑刪除選單,直接巢狀item佈局即可使用,使用十分簡單。
- 1.6 支援插入或者刪除某條資料,支援CoordinatorLayout炫酷的效果
- 1.7 支援貼上頭部的需求效果
- 1.8 RecyclerView實現條目Item拖拽排序與滑動刪除
9.2 具備的優勢分析
- 自定義支援上拉載入更多,下拉重新整理,支援自由切換狀態【載入中,載入成功,載入失敗,沒網路等狀態】的控制元件,拓展功能[支援長按拖拽,側滑刪除]可以選擇性新增 。具體使用方法,可以直接參考demo。
- 輕量級側滑刪除選單,支援recyclerView,listView,直接巢狀item佈局即可使用,整個側滑選單思路是:跟隨手勢將item向左滑動
10.針對阿里VLayout程式碼分析
11.版本更新說明
- v1.0.0 2016年5月5日
- v1.1.0 更新於2017年2月1日
- v1.1.1 更新於2017年6月9日
- v2.0.0 更新於2018年9月26日