优雅(暴力)解决RecyclerView 嵌套RecyclerV
RecyclerView 是一个高度自由可定制的列表组件,它的复用性流畅性是很好的,但是不恰当的使用也会造成一个些困扰。我最近在写一个购物车的页面,由于需求摆在那里,即便不想使用嵌套,但是似乎也没有什么良策,于是乎就闷着头做了。
效果是出来了,但是存在两个问题
- 内层rv 滑动的时候导致图片加载错乱,甚至某些item直接不显示图片
-
上下滑动整体页面,发现越来越卡,直至出现系统出现ANR弹窗
这两个问题困扰了我好多天,首先是第一个,图片错乱甚至是不显示,我起初认为是由于Rv 嵌套Rv 导致内层的rv 数据显示不全,照着这个方向,百度一番,按照网上的说法做了很多尝试:
-
内层rv改成被相对布局包裹,rv的高度自适应,并且相对布局屏蔽rv的焦点descendantFocusability,事实证明在,这个是无效的,即便奏效也是在低版本手机上,7.0以上问题依旧存在
2.修改内外层Rv的Inflate:
外层
@Override
protected DescHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
return new DescHolder(mInflater.inflate(R.layout.layout_shaopcar_item, parent, false));
}
内层
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View inflate = LayoutInflater.from(mContext).inflate(R.layout.layout_recy_item_goods_info_new, null);
return new ViewHolder(inflate);
}
enm,这种方法吧,怎么说呢,在我看来就是骚操作,治标不治本,很久以前用过这种,不建议,并且,因为里层的rv在解析布局的时候没有parent和是否依附parent参数的约束,很容易就整体布局歪歪扭扭的,体验非常不好,不多我是发现,把内层的写成:
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View inflate = LayoutInflater.from(mContext).inflate(R.layout.layout_recy_item_goods_info_new, null,parent,true);
return new ViewHolder(inflate);
}
倒是误打误撞解决了问题,但是随着rv的滑动和复用,很可能会再次出现错乱
3.重写recyclerview 并且重写onMeasure
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,MeasureSpec.AT_MOST);
super.onMeasure(widthSpec, expandSpec);
}
感兴趣的朋友可以试试,意义真的不大。
4.就剩下最后一招了,重写布局管理者,在测量的时候改变下,来计算每个item进行显示
public class FullyLinearLayoutManager extends LinearLayoutManager {
private static final String TAG = FullyLinearLayoutManager.class.getSimpleName();
public FullyLinearLayoutManager(Context context) {
super(context);
}
public FullyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
private int[] mMeasuredDimension = new int[2];
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
Log.i(TAG, "onMeasure called. \nwidthMode " + widthMode + " \nheightMode " + heightSpec + " \nwidthSize "
+ widthSize + " \nheightSize " + heightSize + " \ngetItemCount() " + getItemCount());
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++) {
measureScrapChild(recycler, i, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), mMeasuredDimension);
if (getOrientation() == HORIZONTAL) {
width = width + mMeasuredDimension[0];
if (i == 0) {
height = mMeasuredDimension[1];
}
} else {
height = height + mMeasuredDimension[1];
if (i == 0) {
width = mMeasuredDimension[0];
}
}
}
switch (widthMode) {
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
switch (heightMode) {
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
setMeasuredDimension(width, height);
}
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec,
int[] measuredDimension) {
try {
View view = recycler.getViewForPosition(0);// fix
// 动态添加时报IndexOutOfBoundsException
if (view != null) {
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, getPaddingLeft() + getPaddingRight(),
p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, getPaddingTop() + getPaddingBottom(),
p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
}
这个嘛,真的吃性能,而且显示是凑合了,但是真的是往后面会很卡,而且会出现空白页面的情况,当然也是拒绝的了。
- 会不会是加载图片的时候出问题了,所以就想到了以前解决Recyclerview里checkBox 刷新后状态混乱的情况,加Tag 获取Tag,每次加载的时候都要对比tag,一致的时候再去设置图片
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
//设置本地资源占位
holder.goodIcon.setImageResource(R.drawable.ic_launcher);
holder.goodIcon.setTag(R.id.goodIcon, "goodsIcon");
if (holder.goodIcon.getTag() != null && holder.goodIcon.getTag(R.id.goodIcon).equals("goodsIcon")) {
Glide.with(mContext)
.load(url)
.into(holder.goodIcon);
}
}
这个方法吗,也是看运气,上面是我复现的伪代码,我也是试过之后感觉无效才删了,哈哈
- RecyclerView 的adapter里重写以下方法,
@Override
public int getItemViewType(int position) {
return position;
}
并且在给RecycleView设置适配器前,要先设置adapter.setHasStableIds(true),这句是表明使用这个,相当于给图片加了一个tag,tag不变的话,不用重新加载图片。但是也有问题,这会使得 列表的 数据项 重复了,所以还要去实现一个方法:
@Override
public long getItemId(int position) {
return position;
}
- 就是检查加载图片加载的情况了,Glide,这个是我常用的,加载的时候要尽量使用占位图和缓存,一般我是这样写的
Glide.with(mContext)
.load(findImag.get(0))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.rectang_holder)
.centerCrop()
.into(itemImage);
这个需要大家根据自己的情况去判断,是不是因为加载图片的方式导致的。
尝试了这么多,最后我也终于找到了自己的问题所在,约束布局ConstraintLayout,没错就这货!
当我无计可施的时候,我突然想到了会不会是布局出问题了,于是把约束布局全都换成的线性布局,奇迹就出现了,错乱的布局显示正常了,这样我挺意外的,约束布局的初衷是为了解决布局嵌套太深,怎么还带来了坑,其实可以从源码中得到一些启示
先看约束布局;
public class ConstraintLayout extends ViewGroup {
static final boolean ALLOWS_EMBEDDED = false;
-----------省略一部分-----------
private void setChildrenConstraints() {
if (this.mConstraintSet != null) {
this.mConstraintSet.applyToInternal(this);
}
int count = this.getChildCount();
this.mLayoutWidget.removeAllChildren();
for(int i = 0; i < count; ++i) {
View child = this.getChildAt(i);
ConstraintWidget widget = this.getViewWidget(child);
if (widget != null) {
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams)child.getLayoutParams();
widget.reset();
widget.setVisibility(child.getVisibility());
widget.setCompanionWidget(child);
this.mLayoutWidget.add(widget);
if (!layoutParams.verticalDimensionFixed || !layoutParams.horizontalDimensionFixed) {
this.mVariableDimensionsWidgets.add(widget);
}
if (layoutParams.isGuideline) {
android.support.constraint.solver.widgets.Guideline guideline = (android.support.constraint.solver.widgets.Guideline)widget;
if (layoutParams.guideBegin != -1) {
guideline.setGuideBegin(layoutParams.guideBegin);
}
if (layoutParams.guideEnd != -1) {
guideline.setGuideEnd(layoutParams.guideEnd);
}
if (layoutParams.guidePercent != -1.0F) {
guideline.setGuidePercent(layoutParams.guidePercent);
}
再看Recyclerview
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
static final String TAG = "RecyclerView";
static final boolean DEBUG = false;
static final boolean VERBOSE_TRACING = false;
-------------------------省略一部分-----------------------------
private void initChildrenHelper() {
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
@Override
public int getChildCount() {
return RecyclerView.this.getChildCount();
}
@Override
public void addView(View child, int index) {
if (VERBOSE_TRACING) {
TraceCompat.beginSection("RV addView");
}
RecyclerView.this.addView(child, index);
if (VERBOSE_TRACING) {
TraceCompat.endSection();
}
dispatchChildAttached(child);
}
可以看到约束布局和Rv都是继承自viewGroup,但是,Rv还同时实现了ScrollView和NestedScrollingChild ,相应的就是需要处理滑动时间和事件分发,那么约束布局的设计初衷是减少嵌套,约束布局不建议用在有滑动控件的情况下,这是在约束布局设计的时候就是这样设计的。因为约束布局里面的每个控件的位置都是被约束给相对锁定的。
但是我的结构确实是复杂了点,外层Rv的item跟布局是约束布局,下面又存在一个Rv,这样会导致事件分发和处理变得很耗时,也就造成了卡顿和错乱。
好了,解决了Rv滑动错乱的问题,我们再来解决另一头恶魔---ANR
说实话,在这之前我是没想到会让我遇到ANR的问题,我是很注意bitemap的回收以及数据库游标的关闭,也不会在主线程执行耗时操作,这怎么就出现了ANR呢,赫然的一个弹窗,像是赤裸裸的讽刺哇
image.png一般来说Android上造成ANR无非是以下几种情况:
1.按键和触摸事件5s内没被处理完
2.广播:Broadcast ,前台广播为10s处理时间,后台广播为60s处理时间,未在规定时间内完成就会造成ANR
3.service服务: 前台服务20s,后台200s未完成启动
4.内容提供者ContentProvider的publish在10s内没进行完
既然出现了ANR就要从以下几个方面考虑了:
1.主线程在做一些耗时的工作
2.主线程被其他线程锁
3.cpu被其他进程占用,该进程没被分配到足够的cpu资源。
逐一排除后,我发现,
我的卡顿和ANR完全是因为Rv嵌套Rv,在滑动的时候处理事件无法及时响应造成的。这多亏了Android studio 的profile
image.png
通过检测cpu性能逐步recode到了问题所在。
但是存在一个问题,似乎对于我现在需要的需求的来说,除了嵌套,似乎也没有什么好的方法。百度一番,有说重写Recyclerview解决的;
public class MyRecycleView extends RecyclerView {
public MyRecycleView(Context context) {
super(context);
}
public MyRecycleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyRecycleView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
//返回false,则把事件交给子控件的onInterceptTouchEvent()处理
return false;
}
@Override
public boolean onTouchEvent(MotionEvent e) {
//返回true,则后续事件可以继续传递给该View的onTouchEvent()处理
return true;
}
}
但是这样就掉坑了,哈哈,恭喜你,你的外层rv将不能滑动了。
image.png
所以啊,尽量不要使用Rv嵌套Rv,否则进坑容易跳坑难,但是呢,如果非要这么做,也不是不能解决,我们还可以通过以下的暴力手段去优雅的解决掉嵌套带来的卡顿和AN**
//优化嵌套卡顿
shoppingCar.setHasFixedSize(true);
shoppingCar.setNestedScrollingEnabled(false);
shoppingCar.setItemViewCacheSize(600);
RecyclerView.RecycledViewPool recycledViewPool = new
RecyclerView.RecycledViewPool();
shoppingCar.setRecycledViewPool(recycledViewPool);
- setHasFixedSize,作用在于当知道Adapter内Item的改变不会影响RecyclerView宽高的时候,可以设置为true让RecyclerView避免重新计算大小。
- setNestedScrollingEnabled 这个是在处理滑动卡顿时常用的,牵扯到时间分发和手势,不再赘述
- setItemViewCacheSize 是设置子视图的缓存处理大小,这里为了立杆见影,我设置成了600,哈哈,一般200就行
-
recycledViewPool 则是重新给定义个新的存放视图的pool
好了,目前就是这么多,时间仓促,总结不周,如果有不准确的地方可以私信我,欢迎交流。
看看时间,22点多了,洗洗睡了