工作中遇到一些問(wèn)題胖笛,以此記錄問(wèn)題的解決過(guò)程网持。
起因
上周因?yàn)闃I(yè)務(wù)需要,要完成一個(gè)展示優(yōu)惠券信息的列表匀钧,列表內(nèi)每張券都有詳細(xì)信息翎碑,點(diǎn)擊詳細(xì)信息或者右面向下的箭頭,可以展開(kāi)相應(yīng)優(yōu)惠券的詳細(xì)信息之斯。展開(kāi)的同時(shí)添加兩個(gè)動(dòng)畫日杈,展開(kāi)的布局需要做緩慢展開(kāi)的動(dòng)畫,向下展開(kāi)的箭頭需要做順時(shí)針180度旋轉(zhuǎn)變成向上收縮的狀態(tài)佑刷。
當(dāng)時(shí)看到這覺(jué)得沒(méi)問(wèn)題莉擒,一個(gè)RecyclerView就搞定了,在Adapter內(nèi)對(duì)Item布局內(nèi)的View做一個(gè)屬性動(dòng)畫瘫絮,簡(jiǎn)單省事涨冀。于是就開(kāi)始愉快的敲著鍵盤寫了起來(lái),等寫好一測(cè)試麦萤,Perfect鹿鳖!
展開(kāi)收起展開(kāi)毫無(wú)問(wèn)題,刷新一下壮莹,(⊙o⊙)…問(wèn)題來(lái)了翅帜,怎么箭頭是向上的,我記得在onBindViewHolder里已經(jīng)設(shè)置Item中箭頭的狀態(tài)是向下的命满。趕緊Debug一下涝滴,的確是設(shè)置了向下的圖片。后來(lái)又分別展開(kāi)了幾個(gè)Item,刷新了一次列表歼疮,發(fā)現(xiàn)每次箭頭方向錯(cuò)亂的位置還不固定杂抽。立馬反應(yīng)過(guò)來(lái),估計(jì)是條目復(fù)用出的問(wèn)題韩脏。立馬開(kāi)始查RecyclerView的Item緩存機(jī)制缩麸。
RecyclerView條目緩存機(jī)制
看了源碼才發(fā)現(xiàn),RecyclerView緩存基本上是通過(guò)三個(gè)內(nèi)部類管理的骤素,Recycler匙睹、RecycledViewPool和ViewCacheExtension。
** Recycler:**
Recycler用于管理已經(jīng)廢棄或者與RecyclerView分離的ViewHolder济竹,為了方便理解這個(gè)類,整理了下面的資料霎槐,請(qǐng)結(jié)合Recycler的代碼分析:
內(nèi)部類的成員變量和他們的含義:
變量 | 作用 |
---|---|
mChangedScrap | 與RecyclerView分離的ViewHolder列表 |
mAttachedScrap | 未與RecyclerView分離的ViewHolder列表 |
mCachedViews | ViewHolder緩存列表 |
mViewCacheExtension | 開(kāi)發(fā)者可以控制的ViewHolder緩存的幫助類 |
mRecyclerPool | ViewHolder緩存池 |
代碼里面有個(gè)關(guān)鍵的方法送浊,注釋來(lái)自引文:
ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// 在還未detach的廢棄視圖中查找出來(lái)一個(gè)類型匹配(無(wú)效類型)的view.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
if (type != INVALID_TYPE && holder.getItemViewType() != type) {
break;
}
// 表明這個(gè)ViewHolder是從廢棄的View集合中取出來(lái)的,可用于itemView的返回值丘跌。
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
if (!dryRun) {
// 找到已經(jīng)隱藏袭景,但是未被刪除的view,然后將其detach掉闭树,detach scrap中耸棒。
View view = mChildHelper.findHiddenNonRemovedView(position, type);
if (view != null) {
final ViewHolder vh = getChildViewHolderInt(view);
mChildHelper.unhide(view);
int layoutIndex = mChildHelper.indexOfChild(view);
mChildHelper.detachViewFromParent(layoutIndex);
scrapView(view);
vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
return vh;
}
}
// 在第一級(jí)視圖緩存中查找.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type +
") found match in cache: " + holder);
}
return holder;
}
}
return null;
}
RecycledViewPool:
RecycledViewPool類是用來(lái)緩存Item用,是一個(gè)ViewHolder的緩存池报辱,如果多個(gè)RecyclerView之間用setRecycledViewPool(RecycledViewPool)
設(shè)置同一個(gè)RecycledViewPool与殃,他們就可以共享Item。其實(shí)RecycledViewPool的內(nèi)部維護(hù)了一個(gè)Map碍现,里面以不同的viewType為Key存儲(chǔ)了各自對(duì)應(yīng)的ViewHolder集合幅疼。可以通過(guò)提供的方法來(lái)修改內(nèi)部緩存的Viewholder昼接。
下面來(lái)看下這個(gè)類的代碼:
public static class RecycledViewPool {
private SparseArray<ArrayList<ViewHolder>> mScrap =
new SparseArray<ArrayList<ViewHolder>>();
private SparseIntArray mMaxScrap = new SparseIntArray();
private int mAttachCount = 0;
private static final int DEFAULT_MAX_SCRAP = 5;
public void clear() {
mScrap.clear();
}
public void setMaxRecycledViews(int viewType, int max) {
mMaxScrap.put(viewType, max);
final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
if (scrapHeap != null) {
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
}
public ViewHolder getRecycledView(int viewType) {
final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
if (scrapHeap != null && !scrapHeap.isEmpty()) {
final int index = scrapHeap.size() - 1;
final ViewHolder scrap = scrapHeap.get(index);
scrapHeap.remove(index);
return scrap;
}
return null;
}
int size() {
int count = 0;
for (int i = 0; i < mScrap.size(); i ++) {
ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i);
if (viewHolders != null) {
count += viewHolders.size();
}
}
return count;
}
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapHeapForType(viewType);
if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
void attach(Adapter adapter) {
mAttachCount++;
}
void detach() {
mAttachCount--;
}
/**
* Detaches the old adapter and attaches the new one.
* <p>
* RecycledViewPool will clear its cache if it has only one adapter attached and the new
* adapter uses a different ViewHolder than the oldAdapter.
*
* @param oldAdapter The previous adapter instance. Will be detached.
* @param newAdapter The new adapter instance. Will be attached.
* @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same
* ViewHolder and view types.
*/
void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
boolean compatibleWithPrevious) {
if (oldAdapter != null) {
detach();
}
if (!compatibleWithPrevious && mAttachCount == 0) {
clear();
}
if (newAdapter != null) {
attach(newAdapter);
}
}
private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
ArrayList<ViewHolder> scrap = mScrap.get(viewType);
if (scrap == null) {
scrap = new ArrayList<ViewHolder>();
mScrap.put(viewType, scrap);
if (mMaxScrap.indexOfKey(viewType) < 0) {
mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
}
}
return scrap;
}
}
這個(gè)類提供了四個(gè)公共方法:
返回值 | 方法 | 作用 |
---|---|---|
void | clear() | 清空緩存池 |
RecyclerView.ViewHolder | getRecycledView(int viewType) | 得到一個(gè)viewType類型的Item |
void | putRecycledView(RecyclerView.ViewHolder scrap) | 把viewType類型的Item放入緩存池 |
void | setMaxRecycledViews(int viewType, int max) | 設(shè)置對(duì)應(yīng)viewType類型的Item的最大緩存數(shù)量 |
ViewCacheExtension:
我們先來(lái)看下代碼:
public abstract static class ViewCacheExtension {
/**
* Returns a View that can be binded to the given Adapter position.
* <p>
* This method should <b>not</b> create a new View. Instead, it is expected to return
* an already created View that can be re-used for the given type and position.
* If the View is marked as ignored, it should first call
* {@link LayoutManager#stopIgnoringView(View)} before returning the View.
* <p>
* RecyclerView will re-bind the returned View to the position if necessary.
*
* @param recycler The Recycler that can be used to bind the View
* @param position The adapter position
* @param type The type of the View, defined by adapter
* @return A View that is bound to the given position or NULL if there is no View to re-use
* @see LayoutManager#ignoreView(View)
*/
abstract public View getViewForPositionAndType(Recycler recycler, int position, int type);
}
ViewCacheExtension的代碼一看什么都沒(méi)有爽篷,沒(méi)錯(cuò)這是一個(gè)需要開(kāi)發(fā)者重寫的類。上面Recycler里調(diào)用Recycler.getViewForPosition(int)
方法獲取View時(shí)慢睡,Recycler先檢查自己內(nèi)部的attached
scrap
和一級(jí)緩存逐工,再檢查ViewCacheExtension.getViewForPositionAndType(Recycler, int, int)
,最后檢查RecyclerViewPool漂辐,從上面三個(gè)任何一個(gè)只要拿到View就不會(huì)調(diào)用下一個(gè)方法泪喊。所以我們可以重寫getViewForPositionAndType(Recycler recycler, int position, int type)
,在方法里通過(guò)Recycler類控制View緩存者吁。注意:如果你重寫了這個(gè)類窘俺,Recycler不會(huì)在這個(gè)類中做緩存View的操作,是否緩存View完全由開(kāi)發(fā)者控制。
總結(jié)
經(jīng)過(guò)上面的分析瘤泪,發(fā)現(xiàn)被屬性動(dòng)畫修改過(guò)的ImageView在holder里灶泵,被RecyclerView緩存了之后,在別的Item又拿出來(lái)復(fù)用对途,雖然你設(shè)置了向下的背景圖片赦邻,但是這個(gè)ImageView是做過(guò)180旋轉(zhuǎn)的,所以設(shè)置一個(gè)向下的箭頭圖片還是向上的樣子实檀。
看來(lái)以后像旋轉(zhuǎn)一類的簡(jiǎn)單的動(dòng)畫還是用View動(dòng)畫就可以了惶洲,復(fù)雜的動(dòng)畫再用屬性動(dòng)畫。也可以重寫Adapter里的void onViewDetachedFromWindow(VH holder)
方法膳犹,在里面拿到holder找到修改過(guò)的ImageView恬吕,恢復(fù)他原來(lái)的屬性,特別是有View被緩存復(fù)用的時(shí)候一定記得恢復(fù)原來(lái)的屬性须床,否則就會(huì)出現(xiàn)這種混亂的情況铐料。
引用
部分內(nèi)容來(lái)自以下博客,特此鳴謝博客作者的分享:
RecyclerView解析
RecyclerView源碼分析
本文內(nèi)容采用 CC BY-NC-SA 3.0 進(jìn)行許可, 轉(zhuǎn)載請(qǐng)注明出處, 版權(quán)歸本人及所有貢獻(xiàn)者所有豺旬。