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滑动卡顿