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的缓存机制,后续文章再和大家分享这个主题。

猜你喜欢

转载自blog.csdn.net/qinhai1989/article/details/83895430