ListView嵌套RecycleView滑动卡顿问题的优化方案

Posted Vigibord

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ListView嵌套RecycleView滑动卡顿问题的优化方案相关的知识,希望对你有一定的参考价值。

抛出问题

ListView嵌套RecycleView(或者ListView、GridView)时会存在性能问题,是由于内层RecycleView做为外层Listview的item加载时,该RecycleView又会一次性加载它自身的子item项,子item越复杂、手机性能越差,滑动时卡顿现象越明显、越不流畅。
RecycleView虽然自身有RecycleViewPool的概念,可以多个RecycleView共用一个RecycleViewPool,但是也存在不能预加载及延时间隔刷新的功能
使用场景如图:

优化方案

A.使用自定义的水平ScrollView 嵌套线性布局(NestFullListView) 替代 RecycleView,实现可以预加载及延时间隔刷新item的功能;
B.当竖向的Listview加载到NestFullListView项时,延时刷新里面的每个子item(间隔50ms),只刷新屏幕可见子item,当用户滑动NestFullListView时,再刷新其他项;
C.ListView在初始化adapter时预加载N个NestFullListView(NestFullListView的子item也预加载),解决滑动时,recycleview初始化时一次性加载所有的可见item,其inflate、onlayout及bindviewholder等耗时太多导致的卡顿问题
D.NestFullListView支持adapter和viewholder,支持动态加载
F.UI效果保持和recycleview的保持一致

具体实现:

1.预加载NestFullListView及其子item

private void preLoadHorizontalCard() 
        mHCardList = new ArrayList<>();
        LayoutInflater inflater = LayoutInflater.from(mContext);
        for(int i = 0 ; i< PRELOAD_LISTVIEW_COUNT; i++) 
            NestFullListView view = (NestFullListView) inflater.inflate(R.layout.nestlist, null);
            view.preLoad();
            mHCardList.add(view);
        
    

2.获取预加载的NestFullListView并替换listview项中的NestFullListView
依次取出来用

private NestFullListView getCardView()
        if(mCurCard < PRELOAD_LISTVIEW_COUNT)
            NestFullListView view = mHCardList.get(mCurCard);
            mCurCard++;
            if(LogUtils.isDebug()) 
                LogUtils.d("NestFullListView", "getCardView from preload:" + view);
            
            return view;
        
        return null;

在listview的adapter中bindholder时替换预加载好的NestFullListView

NestFullListView recycler = (NestFullListView) holder.getView(R.id.nest_view);
        MyHorizontalScrollView parent = (MyHorizontalScrollView) holder.getView(R.id.nest_view_parent);
        if(recycler.getChildCount() == 0)
            NestFullListView preloadRecycle = getCardView();
            if(preloadRecycle != null) 
                parent.removeView(recycler);
                recycler = preloadRecycle;
                holder.putView(R.id.nest_view, recycler);
                parent.addView(recycler);
            
        
        recycler.setFocusable(false);

        if (recycler.getAdapter() == null) 
            GroupItemAdapter groupitemadapter = new GroupItemAdapter(mContext, recommendData.mList, R.layout.nest_view_item);
            recycler.setAdapter(groupitemadapter);
         else 
            Integer lasScroll = positionScrolls.get(position);
            int lastResult = lasScroll == null? 0 : lasScroll;
            recycler.setList(recommendData.mList, (lastResult != 0));
        

记录scrollview的位置

public void resetRecyclerPosition(final MyHorizontalScrollView recycler, final NestFullListView nestFullListView, final int position)
        recycler.setOnScrollListener(new MyHorizontalScrollView.OnScrollListener() 
            @Override
            public void onScrollChanged(int l, int t, int oldl, int oldt) 
                if(nestFullListView != null)
                    nestFullListView.showAllItem();
                
                if(l != oldl) 
                    positionScrolls.put(position, l);
                
            
        );
        int curScroll = recycler.getScrollX();
        Integer lasScroll = positionScrolls.get(position);
        int lastResult = lasScroll == null? 0 : lasScroll;
        if(lastResult - curScroll != 0)
            recycler.scrollBy(lastResult - curScroll, 0);
        
    

3.延时间隔刷新子item的方法在NestFullListView类里面,这里不贴代码了;

上代码

NestFullListView.java

public class NestFullListView extends LinearLayout 

    private static final String TAG = "NestFullListView";
    private static final int DEFAULT_SHOW_COUNT = 3;
    private static final int DEFAULT_PRELOAD_COUNT = 3;
    private static int CALCULATE_SHOW_COUNT = 0;
    private static final long SHOW_DELAY_TIME = 50;
    private static int MARGIN_1P5 = DensityUtils.dip2px(1.5f);

    private LayoutInflater mInflater;
    private List<RecyclerViewAdapter.BaseRecyclerViewHolder> mVHCahces;//缓存ViewHolder,按照add的顺序缓存,
    private Handler mHandler;
    private AppRecyclerViewAdapter mAdapter;
    private int mCurrentItem;
    private boolean isShowAll = false;


    public NestFullListView(Context context) 
        super(context);
        init(context);
    

    public NestFullListView(Context context, AttributeSet attrs) 
        super(context, attrs);
        init(context);
    

    private void init(Context context) 
        mInflater = LayoutInflater.from(context);
        mVHCahces = new ArrayList<>();
        mHandler = new Handler();
        calculateShowCount(context);
    

    private void calculateShowCount(Context context)
        if(CALCULATE_SHOW_COUNT == 0)
            CALCULATE_SHOW_COUNT = ( DeviceUtils.getScreenWidth(context) / context.getResources().getDimensionPixelSize(R.dimen.recommend_group_list_item_width) ) + 1;
            if(LogUtils.isDebug())
                LogUtils.d(TAG, "calculateShowCount :" + CALCULATE_SHOW_COUNT);
            
        
    

    /**
     * 外部调用  同时刷新视图
     *
     * @param mAdapter
     */
    public void setAdapter(AppRecyclerViewAdapter mAdapter) 
        this.mAdapter = mAdapter;
        mCurrentItem = 0;
        initUI();
        mHandler.post(mUpdateRunnable);
    

    Runnable mUpdateRunnable = new Runnable() 
        @Override
        public void run() 
            if (mCurrentItem >= mAdapter.getItemCount()) 
                if(LogUtils.isDebug()) 
                    LogUtils.d(TAG, "mCurrentItem >= mAdapter.getDatas().size()");
                
                return;
            

            if(!isShowAll && mCurrentItem >= ( CALCULATE_SHOW_COUNT == 0 ? DEFAULT_SHOW_COUNT : CALCULATE_SHOW_COUNT))
                if(LogUtils.isDebug()) 
                    LogUtils.d(TAG, "!isShowAll && mCurrentItem >= DEFAULT_SHOW_COUNT");
                
                return;
            

            updateUI(mCurrentItem);
            mCurrentItem ++;

            mHandler.postDelayed(mUpdateRunnable, SHOW_DELAY_TIME);
        
    ;

    private void initUI()
        if(LogUtils.isDebug()) 
            LogUtils.d(TAG, "initUI");
        
        if (null != mAdapter && mAdapter.getItemCount() > 0) 
            //数据源有数据
            int childCount = getChildCount();
            int dataSize = mAdapter.getItemCount();
            if (dataSize >= childCount) //数据源大于现有子View不清空

                for (int j = 0; j < childCount - 1; j++) 
                    getChildAt(j).setVisibility(VISIBLE);
                

             else if (dataSize < childCount) //数据源小于现有子View,删除后面多的
                if(LogUtils.isDebug()) 
                    LogUtils.d(TAG, "removeViews from " + mAdapter.getItemCount() + " count " + (getChildCount() - mAdapter.getItemCount()));
                
                for (int j = dataSize; j < getChildCount(); j++) 
                    getChildAt(j).setVisibility(GONE);
                
            
         else 
            if(LogUtils.isDebug()) 
                LogUtils.d(TAG, "removeAllViews");
            
            removeAllViews();//数据源没数据 清空视图
        
    

    private void updateUI(final int i) 
        if(LogUtils.isDebug()) 
            LogUtils.d(TAG, "updateUI :" + i);
        

        if (null != mAdapter && mAdapter.getItemCount() > 0) 

            if (i < mAdapter.getItemCount()) 
                RecyclerViewAdapter.BaseRecyclerViewHolder holder;
                if (mVHCahces.size() - 1 >= i) //说明有缓存,不用inflate,否则inflate
                    holder = mVHCahces.get(i);
                    if(LogUtils.isDebug()) 
                        LogUtils.d(TAG, "bind item :" + i + " bind from cache");
                    
                 else 
                    holder = mAdapter.onCreateViewHolder(this, 0);
                    if(LogUtils.isDebug()) 
                        LogUtils.d(TAG, "bind item :" + i + " bind from new");
                    
                    mVHCahces.add(holder);//inflate 出来后 add进来缓存
                
                mAdapter.onBindViewHolder(holder, i);
                //如果View没有父控件 添加
                if (null == holder.getConvertView().getParent()) 
                    if(LogUtils.isDebug()) 
                        LogUtils.d(TAG, "bind item :" + i + " add to parent");
                    
                    addViewWithPadding(holder.getConvertView());
                
                if (holder.getConvertView().getVisibility() == GONE) 
                    holder.getConvertView().setVisibility(VISIBLE);
                
             else 
                if(LogUtils.isDebug()) 
                    LogUtils.d(TAG, "i > mAdapter.getDatas().size()");
                
            
         else 
            if(LogUtils.isDebug()) 
                LogUtils.d(TAG, "removeAllViews");
            
            removeAllViews();//数据源没数据 清空视图
        

    

    public AppRecyclerViewAdapter getAdapter()
        return mAdapter;
    

    public void setList(List<ApkResInfo> data, boolean isShowAll)
        mHandler.removeCallbacksAndMessages(null);
        mAdapter.setList(data);
        mCurrentItem = 0;
        initUI();
        this.isShowAll = isShowAll;
        mHandler.post(mUpdateRunnable);
    

    public void showAllItem()
        if(isShowAll == false) 
            if(LogUtils.isDebug()) 
                LogUtils.d(TAG, "showAllItem");
            
            this.isShowAll = true;
            mHandler.post(mUpdateRunnable);
        
    

    public void preLoad()
        if(LogUtils.isDebug()) 
            LogUtils.d(TAG, "preLoad");
        
        for(int i = 0 ; i < DEFAULT_PRELOAD_COUNT; i ++)
            View contentView = LayoutInflater.from(getContext()).inflate(R.layout.recommend_list_group_item, this, false);
            RecyclerViewAdapter.BaseRecyclerViewHolder holder = new RecyclerViewAdapter.BaseRecyclerViewHolder(contentView);
            mVHCahces.add(holder);
            addViewWithPadding(contentView);
        
    

    private void addViewWithPadding(View contentView)
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) contentView.getLayoutParams();
        params.leftMargin = MARGIN_1P5;
        params.rightMargin = MARGIN_1P5;
        contentView.setLayoutParams(params);
        addView(contentView);
    

    @Override
    protected void onDetachedFromWindow() 
        mHandler.removeCallbacksAndMessages(null);
        super.onDetachedFromWindow();
    

RecyclerViewAdapter,ViewHolder

public abstract class RecyclerViewAdapter<T> extends RecyclerView.Adapter<RecyclerViewAdapter.BaseRecyclerViewHolder> 

    protected final Context mContext;
    private final int mItemLayoutId;
    protected List<T> datas;

    public abstract void onConvert(BaseRecyclerViewHolder holder, final T item, int position);

    public RecyclerViewAdapter(Context context, List<T> datas, int itemLayoutId) 
        this.mContext = context;
        this.datas = datas;
        this.mItemLayoutId = itemLayoutId;
    

    @Override
    public BaseRecyclerViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) 
        View contentView = LayoutInflater.from(mContext).inflate(mItemLayoutId, viewGroup, false);
        return new BaseRecyclerViewHolder(contentView);
    

    public void setList(List<T> data) 
        clearHolderCache();
        this.datas = data;
        notifyDataSetChanged();
    


    public void setList(List<T> data,int firstVisiblePosition,int lastVisiblePosition) 
        clearHolderCache();
        if (data != null && datas != null && data.size() == datas.size() && lastVisiblePosition < data.size()) 
            this.datas = data;
            notifyItemRangeChanged(firstVisiblePosition, lastVisiblePosition);
        else
            this.datas = data;
            notifyDataSetChanged();
        
    

    @Override
    public void onBindViewHolder(BaseRecyclerViewHolder holder, int position) 
        onConvert(holder, datas.get(position), position);
    

    @Override
    public int getItemCount() 
        return datas == null ? 0 : datas.size();
    

    protected void clearHolderCache() 

    

    public static class BaseRecyclerViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener 

        private final SparseArray<View> mViews;
        private final View mConvertView;
        private OnItemClickListener mOnItemClickListener;

        public BaseRecyclerViewHolder(View itemView) 
            super(itemView);
            mConvertView = itemView;
            mConvertView.setOnClickListener(this);
            mViews = new SparseArray<>();
        

        public SparseArray<View> getAllHolderViews() 
            return mViews;
        

        public View getConvertView() 
            return mConvertView;
        

        /**
         * 对外部提供获取对应view的方法
         */
        public View getView(int viewId) 
            return retrieveView(viewId);
        

        private View retrieveView(int viewId) 
            View view = mViews.get(viewId);
            if (view == null) 
                view = mConvertView.findViewById(viewId);
                mViews.put(viewId, view);
            
            return view;
        

        /**
         * 为TextView设置字符串
         */
        public BaseRecyclerViewHolder setText(int viewId, CharSequence text) 
            TextView view = (TextView) retrieveView(viewId);
            view.setText(text);
            return this;
        

        /**
         * 为ImageView设置图片
         */
        public BaseRecyclerViewHolder setImageByUrl(int viewId, String url) 
            SimpleDraweeView view = (SimpleDraweeView) retrieveView(viewId);
            FrescoImageLoaderHelper.setImageByUrl((SimpleDraweeView) view, url);
            return this;
        

        public BaseRecyclerViewHolder setVisible(int viewId, boolean visible) 
            View view = retrieveView(viewId);
            view.setVisibility(visible ? View.VISIBLE : View.GONE);
            return this;
        

        public BaseRecyclerViewHolder setOnClickListener(int viewId, View.OnClickListener listener) 
            View view = retrieveView(viewId);
            view.setOnClickListener(listener);
            return this;
        

        public BaseRecyclerViewHolder setOnItemClickListener(OnItemClickListener onItemClickListener) 
            mOnItemClickListener = onItemClickListener;
            return this;
        

        @Override
        public void onClick(View v) 
            if (mOnItemClickListener != null)
                mOnItemClickListener.onItemClick();
        

    

    public interface OnItemClickListener 
        void onItemClick();
    

缺点

这种实现方式的不足之处是,在没有被横向滑动时只加载屏幕能显示的子item个数,但是一旦被滑动,将加载所有的子item;如果子item太多也会有性能问题,如果能借鉴listview的方式,只显示可见item与view复用,将会大大提高该种方式的扩展性;
如果有某个大神直接能在listview或者RecycleView的基础上实现预加载及延时间隔刷新item,请@我,万分感谢!

以上是关于ListView嵌套RecycleView滑动卡顿问题的优化方案的主要内容,如果未能解决你的问题,请参考以下文章

滑动冲突问题,触摸事件拦截处理

NestedScrollView嵌套RecycleView 滑动 实现上滑隐藏 下滑显示头部效果

React Image加载图片过大导致ListView滑动卡顿

Android Scrollview嵌套RecyclerView导致滑动卡顿问题解决

Android开发RecycleView的使用全解

android scrollview 嵌套listview 不滑动