RV 两三事

354 阅读2分钟

item gone

在 onBindViewHolder 中设置 holder.itemView gone 是无效的:虽然不会显示 item 的内容,但该占的位置还是占住了,类似 INVISIBLE 效果

原因么,就是 rv 在 layout 各个 item 时没有将 gone item 排除在外。以 LinearLayoutManager 为例看一下源码:

// LinearLayoutManager 

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    // 通过缓存取下一个要显示的 view。涉及到 onCreateVH,onBindVH 之类的
    View view = layoutState.next(recycler);
    // 对 view 发起 measure 过程
    measureChildWithMargins(view, 0, 0);
    
    // 获取当前 view 一共消耗了多少空间。以垂直为例:
    // 返回的是 item 高度 + 垂直 margin + ItemDecoration#getItemOffsets 设置的垂直值
    // 从这里可以看出,无论 view 有没有 gone,rv 都会把它应占据的空间给留出来
    // 可以与 LinearLayout 对比看:LinearLayout 在 layout 子 view 时会判断子 view 是不是 gone
    // 神奇的一逼,不知道为啥这么处理
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    // .....
    // 发起 layout 过程
    layoutDecoratedWithMargins(view, left, top, right, bottom);
}

解决办法:将影响高度的三个因素都修改成 0 即可

notifyDataSetChanged 与 notifyItemChanged

  1. notifyDataSetChanged:所有 item 都会放到 pool 缓存中(如果能放得下的话),所以 vh 会复用但需要重新执行 onBindViewHolder
  2. 后者,影响范围内的会放到 changeScrap 中,其余的会放到 attachScrap 中。这里说一下两个 scrap 的区别:change 只在预布局时使用,其中缓存的 vh 最终会进到 pool 中

两个方法最终都会调用 requestLayout,从而会执行 layout 过程,然后就会到 LayoutManager#onLayoutChildren,该方法会调用 detachAndScrapAttachedViews(),该方法会倒序遍历所有 child,对它们进行回收

public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
    // RV 本身是 ViewGroup
    final int childCount = getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
        final View v = getChildAt(i);
        scrapOrRecycleView(recycler, i, v);
    }
}

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    
    // 这里的 If 判断就是 notifyDataSetChanged 与 notifyItemChanged 的分歧
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
            
        // notifyDataSetChanged 走这里,最终 vh 会加到 pool 缓存中
        removeViewAt(index);
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        detachViewAt(index);
        // 这里会将 vh 加到 mAttachedScrap 或者 mChangedScrap
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

上面就是两个方法导致 vh 回收时的逻辑。再结合 vh 的复用,就可以得出两者的区别。缓存的使用逻辑如下:onLayoutChildren() 会调用到 layoutChunk(),最终到 RecyclerView.Recycler#next() 拿到 vh(有可能是新建,也有可能是从缓存中拿),具体代码分析省略。