问题是什么
问题还要从接入百度广告说起,这里要说的是百度原生广告,接入过广告的同学可能知道,接入广告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的缓存机制,后续文章再和大家分享这个主题。