关于RecyclerView你知道的不知道的都在这了
RecyclerView问题汇总
RecyclerView汇总-YCBlogs
A flexible view for providing a limited window into a large data set
一个灵活的视图,为有限的窗口展示大量数据
- 默认支持Linear、Grid、Staggered Grid布局
- 强制实现ViewHolder,减少findViewById
- 解耦架构
public class UnPackAdapter extends RecyclerView.Adapter<UnPackViewHolder> {
@Override
public UnPackViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
UnPackViewHolder viewHolder = new UnPackViewHolder(itemView, mCount++);
return viewHolder;
}
@Override
public void onBindViewHolder(UnPackViewHolder holder, int position) {
holder.tvCenter.setText(mDatas.get(position));
}
}
//封装同ListView...
class UnPackViewHolder extends RecyclerView.ViewHolder{
public TextView tvCenter;
public UnPackViewHolder(View itemView, int count) {
tvCenter = itemView.findViewById(R.id.tv_common_recycler_center);
}
}
一、 ViewHolder
与ListView不同的是,强制实现
与ListView相同的是,是否复用跟ViewHolder没有关系。复用机制是控件内部的算法机制
- ViewHoler和item View一对一对应
- 减少findViewById
二、 使用场景
2.1 Item多布局
- 在getItemViewType中返回不同的类型
- 在onCreateViewHolder、onBindViewHolder中做相应的处理
@Override
public int getItemViewType(int position) {
if(headerHolder != null && position == 0) return ITEM_TYPE_HEADER;
return ITEM_TYPE_NOMAL;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType == ITEM_TYPE_HEADER) return headerHolder;
View view = LayoutInflater.from(mContext).inflate(layId, null);
MyCommonHolder holder = new MyCommonHolder(view);
return holder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(getItemViewType(position) == ITEM_TYPE_NOMAL){...}
if(getItemViewType(position) == ITEM_TYPE_HEADER && headerHolder != null){...}
}
2.2 嵌套、滑动冲突、高度不完整、卡顿
解决RecyclerView嵌套RecyclerView位移问题
2.2.1 父Recycler被顶一段距离
由于子Recycler抢占焦点,所以把父Recycler顶上去了;
解决:给父Recycler设置抢占焦点
2.2.2 父竖向子横向,滑动子Recycler然后再滑动父Recycler,回到原位置后子Recycler状态复原
因为子Recycler重新显示时,是复用了其它父Item,原来的状态丢失
解决:监听父Recycler滑动,保存每个子Item的位移状态,bind的时候设置恢复
2.2.3 与ScrollView嵌套滑动冲突、显示不完整
- LayoutMana复写canScrollVertically、canScrollHorizontally方法禁止滑动
- 复写ScrollView拦截滑动
- 显示不完整:父布局使用NestedScrollView代替
- recyclerView.setHasFixedSize(true);
- recyclerView.setNestedScrollingEnabled(false)
2.2.4 嵌套卡顿
下面章节
2.3 onBinderViewHolder()
在每次复用Item View时都会调用么?
不是的,有些是在缓存中直接取出的
不同的缓存中取,调用的方法不同
- Scrap、Cache不调用
- RecyclerViewPool,bind但不create
想精确知道每条Item显示的次数
例如做展示统计,此时用onBinderViewHolder是不太准确的 onViewAttachedToWindow每次显示ItemView都会被调用
三、 优化
3.1 更新
notifyItem...
尽量使用局部更新,而不是notifyDataSetChanged()全部刷新
DiffUtils:局部/增量更新
DiffUtil计算出将一个数据集转换为另一个的最小更新量,DiffUtil还可以识别一个数据在数据集中的移动。Eugene的算法对控件进行了优化,在查找两个数据集之间最少加减操作时的空间复杂度为O( N),时间复杂度为O(N+D^2)。而如果添加了对数据密集移动的识别,复杂度就会提高到O(N^2)。
所以:
如果数据集中数据不存在转移情况,可以关闭移动识别功能来提高性能。
当数据集巨大时你应该在后台线程计算数据集的更新。
private class DiffCallback extends DiffUtil.Callback {
private List<Item> mOldDatas;
private List<Item> mNewDatas;
//传入旧数据和新数据的集合
public DiffCallback(List<Item> oldDatas,List<Item> newDatas) {
this.mOldDatas = oldDatas;
this.mNewDatas = newDatas;
}
@Override public int getOldListSize() {
return mOldDatas != null ? mOldDatas.size() : 0;
}
@Override public int getNewListSize() {
return mNewDatas != null ? mNewDatas.size() : 0;
}
/**
* 被DiffUtil调用,用来判断 两个对象是否是相同的Item。
* 例如,如果你的Item有唯一的id字段,这个方法就 判断id是否相等。本例判断id字段是否一致
*/
@Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldDatas.get(oldItemPosition).id == mNewDatas.get(newItemPosition).id;
}
/*
* 被DiffUtil调用,用来检查 两个item是否含有相同的数据
* 这个方法仅仅在areItemsTheSame()返回true时,才调用。
*/
@Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
String oldName = mOldDatas.get(oldItemPosition).getName();
String newName = mNewDatas.get(newItemPosition).getName();
if (!oldName.equals(newName)) {
return false;
}
return true;
}
/**
* areItemsTheSame()返回true而areContentsTheSame()返回false,也就是说两个对象代表的数据是一条,但是内容更新了。
* 注意:实现该方法,在onBindViewHolder中实现Item的局部更新
* 不实现该方法,在onBindViewHolder中实现Item的整体更新
*
*/
@Nullable @Override public Object getChangePayload(int oldItemPosition, int newItemPosition) {
String oldItem = mOldDatas.get(oldItemPosition).getName();
String newItem = mNewDatas.get(newItemPosition).getName();
Bundle bundle = new Bundle();
if (!oldItem.equals(newItem)) {
bundle.putString("name",newItem);
}
return bundle;
}
}
//Adapter
//payloads就是DiffUtil.Callback中的getChangePayload方法返回的数据集
@Override
public void onBindViewHolder(DiffItemHolder holder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder,position);
} else {
Bundle bundle = (Bundle) payloads.get(0);
for(String key : bundle.keySet()) {
switch (key) {
case "name":
holder.mInfo.setText((CharSequence) bundle.get(key));
break;
}
}
}
}
//如果计算量大的话,放到子线程计算
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(oldData, newData));
//主线程更新
diffResult.dispatchUpdatesTo(mUnPackAdapter);
3.2 监听
- 在onCreateViewHolder时设置监听
View -- ViewHolder -- Listener, 三者一一对应 - 复用同一个listener
3.3 setHasFixedSize(true)
当知道Adapter内Item的改变不会影响RecyclerView宽高的时候,可以设置为true让RecyclerView避免重新计算大小
Adapter的增删改插方法,会根据mHasFixedSize这个值来判断需要不需要requestLayout();所以这4个方法不会重新绘制;
而notifyDataSetChanged()即使设置了也会重新绘制改变大小
onItemRangeChanged()、onItemRangeInserted()、onItemRangeRemoved()、onItemRangeMoved()
3.4 Prefetch(预取)
- 由于RenderThread,会进行Prefetch
- 只有LinearLayoutManager有setInitialItemPrefetchCount()方法
- 只要在嵌套内部使用有效
3.5 多个RecyclerView共用RecycledViewPool
RecycledViewPool使用
RecycledViewPool的使用和堆内存分析
当多个RecyclerView中的Item类型一样时,可以共享复用pool中的ViewHolder
- 场景:ViewPage + Fragment + 相同类型的ItemView
- 减少创建新的ViewHolder实例数
- 减少inflate、findviewById等过程
RecycledViewPool pool = new RecycledViewPool();
recyclerView1.setRecycledViewPool(pool);
recyclerView2.setRecycledViewPool(pool);
recyclerView3.setRecycledViewPool(pool);
四、性能优化
三个方面:耗时、缓存预取、滑动冲突
1.1 减少creat/bind的调用次数
inflate、findView会消耗资源
- 防止全部刷新,使用notifyItem...
- DiffUtils:局部绑定更新
- 共用RecycledViewPool
- 减少View Type类型,区别不大的使用相同布局,在bind中处理显示区别
1.2 缩短执行时间或子线程计算
- 防止View布局嵌套
- 大计算量放到子线程中处理
2. 缓存、预加载
- 预取:setInitialItemPrefetchCount
3. 处理滑动冲突
- 外部、内部拦截
- NestedScrollView代替并设置Recycler不可滑动
五、缓存-复用
【暂不做深入研究】
四级缓存:Scrap、Cache、extension(自定义缓存)、viewPool
不同于ListView只有scrap缓存
比较直观的区别是onBindViewHolder的调用时机,并不是每次Item重新出现在屏幕的时候调用的:
- 默认的情况下,cache 缓存 2 个 holder,RecycledViewPool 缓存 5 个 holder
- Scrap/Cache View通过position找到缓存,不是脏数据,不onCreate也不bind。
- ViewChacheExtension用户自己定义的策略(不做了解)
- RecycledViewPool是通过ViewType找到缓存的,并且是脏数据,不onCreate但bind。
- 如果想每次Item显示的时候知道:adapter.onViewAttachedToWindow
ListView
1. ViewHolder
不实现ViewHolder一样会复用item view,频繁findView影响性能
- 不是强制实现,只是自定义的封装
- ViewHoler通过tag与convertView一对一对应
- 减少findViewById
// 封装
public class ViewHolder {
private ViewHolder(Context context,ViewGroup parent, int layoutId,int position){
mCovertView = LayoutInflater.from(context).inflate(layoutId, parent,false);
mCovertView.setTag(this);
}
public static ViewHolder get(Context context,View convertView,ViewGroup parent, int layoutId, int position){
if (convertView == null) {
return new ViewHolder(context, parent, layoutId, position);
}else{
ViewHolder holder = (ViewHolder) convertView.getTag();
return holder;
}
}
public <T extends View> T getView(int viewId){
View view = mViews.get(viewId);
if (view == null) {
view = mCovertView.findViewById(viewId);
mViews.put(viewId, view);
}
return (T) view;
}
public ViewHolder setText(int viewId,String text){
getView(viewId).setText(text);
return this;
}
}
其它
getView调用时机
- Active View 不调用(屏幕每16ms刷新,已显示的item不会调用)
- Scrap View 调用
- 做展示统计:getView