CoordinatorLayout 嵌套Recycleview 卡顿问题

2,824 阅读2分钟

1.问题场景

伪代码:
<CoordinatorLayout>
    <AppBarLayout>
      <RecycleView>
      </RecycleView>
   </AppBarLayout>
</ConstraintLayout>

一般这种做法是,底部view的相应滑动,滑动联动,但是同时会出现RecycleView ViewHoder复用失败,造成cpu 的消耗,item到达一定数量后会造成oom页面出现卡顿

2. 问题原理

RecycleView ViewHoder 复用问题第一时间我们应想到是; ViewGrop/onMeasureChild
测量问题,重写 onMeasureChild ,避免中间MeasureSpec.UNSPECIFIED模式 的赋值造成RecycleView的item复用,但是是失败的!

 @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        child.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);
    }

原因是: parentHeightMeasureSpec 已经被设置 MeasureSpec.UNSPECIFIED 测量模式 看下源码CoordinatorLayout onMeasure 局部关键代码:

prepareChildren();

final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
        childHeightMeasureSpec, 0)) {
    onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
            childHeightMeasureSpec, 0);
}

通过prepareChildren()结合LayoutParams

        R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
    mBehavior = parseBehavior(context, attrs, a.getString(
            R.styleable.CoordinatorLayout_Layout_layout_behavior));
}

我们可以得到 Behavior b 就是我们再布局内设置的 AppBarLayout. layout_behavior, 可以看到 Behavior/onMeasureChild 做了一层测量, ,我们继续看 Behavior/onMeasureChild 源码:

@Override
public boolean onMeasureChild(
    @NonNull CoordinatorLayout parent,
    @NonNull T child,
    int parentWidthMeasureSpec,
    int widthUsed,
    int parentHeightMeasureSpec,
    int heightUsed) {
  final CoordinatorLayout.LayoutParams lp =
      (CoordinatorLayout.LayoutParams) child.getLayoutParams();
  if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
    // If the view is set to wrap on it's height, CoordinatorLayout by default will
    // cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
    // what we actually want, so we measure it ourselves with an unspecified spec to
    // allow the child to be larger than it's parent
    parent.onMeasureChild(
        child,
        parentWidthMeasureSpec,
        widthUsed,
        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
        heightUsed);
    return true;
  }

  // Let the parent handle it as normal
  return super.onMeasureChild(
      parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
}

问题找到了问题关键 if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) ,造成了MeasureSpec.UNSPECIFIED 的使用, 而这个模式又会造成Recycleview.LayoutManager加载所有的item,导致复用失败; 看到这 AppBarLayout给固定值或者match_parent 不就解决问题了吗, 是能解决问题,但是这样 我们的layout ui就不符合我们绘制ui的布局了,也会造成页面空白显示问题,所以这样使用recycleview 嵌套是非法使用,矛盾使用!

解决问题

  • 同一使用RecycleView 使用,作为RecycleView item 的一部分,但是也会造成滑动冲突问题,然后通过 NestedScrollingParent3 外部拦截法,来解决内外层的滑动冲突,问题顺利解决
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
    if (e!!.action == MotionEvent.ACTION_DOWN) {
        val childRecyclerView = findCurrentChildRecyclerView()

        // 1. 是否禁止拦截
        doNotInterceptTouchEvent = doNotInterceptTouch(e.rawY, childRecyclerView)

        // 2. 停止Fling
        this.stopFling()
        childRecyclerView?.stopFling()
    }

    return if (doNotInterceptTouchEvent) {
        false
    } else {
        super.onInterceptTouchEvent(e)
    }
}
  • 根据业务场景,也可使用baserecyclerviewadapterhelper,一个优秀的Adapter 框架, addHeaderView来添加itemView,通过 notifyItemInserted(position) 添加ReceiveView 的item
@JvmOverloads
fun addHeaderView(view: View, index: Int = -1, orientation: Int = LinearLayout.VERTICAL): Int {
    if (!this::mHeaderLayout.isInitialized) {
        mHeaderLayout = LinearLayout(view.context)
        mHeaderLayout.orientation = orientation
        mHeaderLayout.layoutParams = if (orientation == LinearLayout.VERTICAL) {
            RecyclerView.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
        } else {
            RecyclerView.LayoutParams(WRAP_CONTENT, MATCH_PARENT)
        }
    }

    val childCount = mHeaderLayout.childCount
    var mIndex = index
    if (index < 0 || index > childCount) {
        mIndex = childCount
    }
    mHeaderLayout.addView(view, mIndex)
    if (mHeaderLayout.childCount == 1) {
        val position = headerViewPosition
        if (position != -1) {
            notifyItemInserted(position)
        }
    }
    return mIndex
}