RecyclerView你所不知道的祕密
問題是什麼
問題還要從接入百度廣告說起,這裡要說的是百度原生廣告,接入過廣告的同學可能知道,接入廣告SDK,廣告的點選事件基本都是由SDK處理的,開發者只需要傳入需要被點選的View即可。雖然這給開發者省了不少事,有時候出了問題,反而會阻礙我們去分析問題。
下面這個介面,就是向百度廣告用來註冊點選事件的介面:
NativeAd.registerViewForInteraction(View view);
我在把廣告接入RecycleView中時,遇到一個坑,初始顯示的時候,點選一切正常,當廣告item被滑出螢幕,然後再次滑回來後,item就點選不了。這是什麼原因呢?真是百思不得其解。
上圖,先看下問題場景:
上面一行4個應用item就是接入的百度廣告資料,其中被我框起來的應用,點選沒有反應了。
問題分析
1、思路一
既然廣告item不能點選,那就去確認點選事件有沒有設定好,這是最直接的思路。RecyclerView.Adapter的onBindViewHolder方法中會呼叫registerViewForInteraction(View)註冊View的點選事件,即使item的View被回收了,下次顯示時會再次註冊事件,正常應該沒有問題才對。需要去跟一下registerViewForInteraction方法的實現,但是百度沒有開放程式碼,只能望而卻步。
這裡貼下onBindViewHolder中的實現程式碼:
@Override public void onBindViewHolder(@NonNull MultiAdsAdapter.ViewHolder holder, final int position) { if (holder.getItemViewType() == TYPE_AD) {//百度廣告型別的資料 NativeAd nativeAd = mList.get(position).getAd(); holder.getTextView().setText(nativeAd.getAdTitle()); if (!TextUtils.isEmpty(nativeAd.getAdIconUrl())) { Glide.with(mContext.getApplicationContext()).load(nativeAd.getAdIconUrl()).diskCacheStrategy(DiskCacheStrategy.NONE).into(holder.getImageView()); } nativeAd.registerViewForInteraction(holder.imageView);//註冊View的點選事件 } else { holder.getTextView().setText("test" + position); holder.getImageView().setImageResource(R.drawable.ic_launcher); } }
2、思路二
通過在adapter生命週期中新增跟蹤日誌,我發現,在item在滑出螢幕時,View會被回收,會被快取起來,在下次再次滑動回來,會使用快取中的View,重新去繫結資料。這個其實是正常的adapterView的實現方案,沒什麼問題啊?理論上確實沒有問題,巧就巧在遇上了百度廣告,這種半吊子SDK,裡面的坑誰踩誰知道。
根據日誌發現,最上面的四個item,在再次顯示時,重用了快取裡面的View,但是View可能不再是之前對應位置的View了。我就想,既然重用有問題,能不能每次滑出螢幕的時候,讓View自動回收了,不讓它重用。
@Override
public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
super.onViewAttachedToWindow(holder);
if (holder.getItemViewType() == TYPE_AD) {
Log.d("MultiAdsAdapter", "onViewAttachedToWindow" + holder.itemView);
}
}
@Override
public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
super.onViewDetachedFromWindow(holder);
if (holder.getItemViewType() == TYPE_AD) {
Log.d("MultiAdsAdapter", "onViewDetachedFromWindow" + holder.itemView + ", isRecyclable = " + holder.isRecyclable());
if (holder.isRecyclable()) {
holder.setIsRecyclable(false);
}
}
}
通過在onViewDetachedFromWindow方法中呼叫holder.setIsRecyclable(false),意思是,是否可回收利用,設定為false後,就是不重用,下次展示時會重新建立View,此方法果然奏效。
但是對於追求高效的我一想,如果不能重用View,RecyclerView的作用何在,於是有了思路三。
3、思路三
根據日誌發現,最上面的四個item,在再次顯示時,重用了快取裡面的View,但是View可能不再是之前對應位置的View了,就是這個位置問題導致了百度廣告無法點選了。既然是位置問題導致,我可不可以,在重新繫結資料時,保證重用的View還是之前使用的那個。於是查閱RecyclerView的方法,果不其然,居然有現成的介面可以設定View快取,雖然還沒去嘗試,但是我感覺已經找到了解決方案。
這個方法就是:
/**
* Sets a new {@link ViewCacheExtension} to be used by the Recycler.
*
* @param extension ViewCacheExtension to be used or null if you want to clear the existing one.
*
* @see ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)
*/
public void setViewCacheExtension(ViewCacheExtension extension) {
mRecycler.setViewCacheExtension(extension);
}
再看下ViewCacheExtension介面引數,RecyclerView會根據position(資料索引)和type(View的型別)去取快取,自己可以去自定義返回的View。
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)
*/
public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}
解決方案
通過對思路三的嘗試,證明此方法可行。
下面就看下ViewCacheExtension的實現:
public class MyViewCacheExtension extends RecyclerView.ViewCacheExtension {
private SparseArray<View> mViewCache;
public MyViewCacheExtension() {
mViewCache = new SparseArray(4);
}
@Override
public View getViewForPositionAndType(RecyclerView.Recycler recycler, int position, int type) {
if (type == MultiAdsAdapter.TYPE_AD
&& mViewCache.size() > position) {
return mViewCache.get(position);
}
return null;
}
public void cacheView(int position, int type, View view) {
if (type != MultiAdsAdapter.TYPE_AD) {
return;
}
if (mViewCache.get(position) != view) {
mViewCache.put(position, view);
}
}
public void clearCache() {
mViewCache.clear();
}
}
那在什麼地方去快取初始的View呢?當然是在onBindViewHolder回撥的時候了
@Override
public void onBindViewHolder(@NonNull MultiAdsAdapter.ViewHolder holder, final int position) {
if (holder.getItemViewType() == TYPE_AD) {
mAdViewCache.cacheView(position, TYPE_AD, holder.itemView);
NativeAd nativeAd = mList.get(position).getAd();
Log.d("MultiAdsAdapter", "onBindViewHolder, position = " + position + ", holder.itemView = " + holder.itemView);
holder.getTextView().setText(nativeAd.getAdTitle());
if (!TextUtils.isEmpty(nativeAd.getAdIconUrl())) {
Glide.with(mContext.getApplicationContext()).load(nativeAd.getAdIconUrl()).diskCacheStrategy(DiskCacheStrategy.NONE).into(holder.getImageView());
}
nativeAd.registerViewForInteraction(holder.imageView);
});
} else {
holder.getTextView().setText("test" + position);
holder.getImageView().setImageResource(R.drawable.ic_launcher);
}
}
哇塞!可以點選了,完美解決。
總結
整合百度廣告到RecyclerView,讓我偶然遇到了這個問題,並且偶然讓我解決了這個問題,使我有所反思,我們常用的RecyclerView,其實我們瞭解的並不夠,有必要研究下它的實現原理,怎麼實現的重用,還有它賴以成名的View的快取機制,後續文章再和大家分享這個主題。