RecyclerView --- (二)自定义LayoutManager

【记录】记录点滴

【需求】Recycler需要特殊排列顺序时,要实现自定义LayoutManager

自定义大致分为三步:1. 放置全部的View;2. 滑动;3. 回收机制

1. RecyclerView继承自ViewGroup,每个 item 就是它的子 view,重新设置子 view的放置位置,就需要重写onLayout。LayoutManager中提供了 onLayoutChildren(),它负责对子 view 布局。

RecyclerView中的源码

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //...
        dispatchLayout();
        //...
    }

    void dispatchLayout() {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

    /**
     * The second layout step where we do the actual layout of the views for the final state.
     * This step might be run multiple times if necessary (e.g. measure).
     */
    private void dispatchLayoutStep2() {
        eatRequestLayout();
        onEnterLayoutOrScroll();
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        // onLayoutChildren may have caused client code to disable item animations; re-check
        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        resumeRequestLayout(false);
    }

从dispatchLayoutStep2的注释中看出,它是真正实现布局(放置子view)的地方,它的内部就调用了LayoutManager的onLayoutChildren。

1)继承RecyclerView.LayoutManager,重写 generateDefaultLayoutParams 和 onLayoutChildren

@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.WRAP_CONTENT,
            RecyclerView.LayoutParams.WRAP_CONTENT);
}
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        super.onLayoutChildren(recycler, state);
        //detach全部子View,并放入缓存
        detachAndScrapAttachedViews(recycler);
        layoutChildren(recycler);
    }

    private void layoutChildren(RecyclerView.Recycler recycler){
        //每个子 view 的 top 位置
        int childTop = 0;
        for(int i = 0; i < getItemCount(); i++){
            //最终调用tryGetViewHolderForPositionByDeadline,要么使用已有的ViewHolder,要么直接创建一个
            View childView = recycler.getViewForPosition(i);
            addView(childView);

            //add后,计算子 view 的宽高
            measureChildWithMargins(childView, 0, 0);

            //宽和高附带了分割线的尺寸,这里没有使用分割线
            int width = getDecoratedMeasuredWidth(childView);
            int height = getDecoratedMeasuredHeight(childView);
            
            //最终调用 view.layoutfan 方法
            layoutDecorated(childView, 0, childTop, width, childTop + height);
            childTop += height;
        }
    }

到这里,至少界面可以展示了。补充下,上述代码在测量view和布局时调用方法不是很严谨,方法如下

//测量时,具体还涉及 getChildMeasureSpec 方法的处理逻辑,只简单描述下是否考虑margin
//测量时,考虑了margin
measureChildWithMargins(childView, 0, 0);
//测量时,不考虑margin
measureChild(childView, 0, 0);

//布局时,考虑margin
layoutDecoratedWithMargins(childView, 0, childTop, width, childTop + height);
//布局时,不考虑margin
layoutDecorated(childView, 0, childTop, width, childTop + height);

2. 滑动,涉及到下面4个方法

//能否横向滑动,返回true,表示可以滑动
canScrollHorizontally()
//能否纵向滑动,返回true,表示可以滑动
canScrollVertically()

//横向滑动的距离,返回int,用于边缘与fling效果
int scrollHorizontallyBy()
//纵向滑动的距离,返回int,用于边缘与fling效果
int scrollVerticallyBy()

一个垂直滑动的示例

    @Override
    public boolean canScrollVertically() {
        return true;
    }
    int verticalScrollOffset = 0;
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        //列表向下滑动dy>0
        //否则dy<0
        int distance = dy;
        if (verticalScrollOffset + dy < 0) {//如果滑动的偏移量<0
            distance = -verticalScrollOffset;
        } else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {//如果滑动后的偏移量超过到最大的偏移量
            distance = totalHeight - getVerticalSpace() - verticalScrollOffset;
        }

        //将竖直方向的偏移量+distance 
        verticalScrollOffset += distance ;

        // 调用该方法通知view在y方向上移动指定距离
        offsetChildrenVertical(-distance);

        return distance;
    }

    private int getVerticalSpace() {
        //计算RecyclerView的可用高度,减去上下Padding值
        return getHeight() - getPaddingBottom() - getPaddingTop();
    }

    private int getHorizontalSpace() {
        //计算RecyclerView的可用高度,减去上下Padding值
        return getHeight() - getPaddingBottom() - getPaddingTop();
    }

到这里,就可以实现滑动了。

扫描二维码关注公众号,回复: 3866697 查看本文章

3. 上面两步实现了布局和滑动,但这时所有的View都在,没有真正利用上回收功能。思路很简单,计算每个 item 的(l,t,r,b)位置信息,滑动时,判断 item 是否在展示区域内,如果已滑出区域则回收,否则继续展示。

1)先修改onLayoutChildren,计算每个 item 的位置信息,所以修改layoutChildren方法

    SparseArray<Rect> allItems = new SparseArray<>();
    //这里稍微做点修改
    //layoutChildern实际只做 item 的测量和 保存位置的工作
    private void layoutChildren(RecyclerView.Recycler recycler){
        int childTop = 0;
        for(int i = 0; i < getItemCount(); i++){
            View childView = recycler.getViewForPosition(i);
            addView(childView);
            measureChildWithMargins(childView, 0, 0);

            int width = getDecoratedMeasuredWidth(childView);
            int height = getDecoratedMeasuredHeight(childView);
            
            //测量后就detach掉
            detachAndScrapView(childView, recycler);
            allItems.put(i, new Rect(0, childTop, width, childTop + height));
            childTop += height;
            totalHeight += height;
        }
        //循环中调用了detachAndScrapView,也可以最后调用detachAndScrapAttachedViews
        //detachAndScrapAttachedViews(recycler);
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        super.onLayoutChildren(recycler, state);
        layoutChildren(recycler);

        // recycleAndFillView 方法根据展示区域和各 item 位置坐标决定是否展示
        recycleAndFillView(recycler, state);
    }

再看看 recycleAndFillView方法

    private void recycleAndFillView(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() <= 0 || state.isPreLayout()) {
            return;
        }

        // 当前scroll offset状态下的显示区域
        Rect displayRect= new Rect(0, verticalScrollOffset, getHorizontalSpace(),
                verticalScrollOffset + getVerticalSpace());

        //重新显示需要出现在屏幕的子View
        for (int i = 0; i < getItemCount(); i++) {
            //判断ItemView的位置和当前显示区域是否重合
            if (Rect.intersects(displayRect, allItems.get(i))) {
                //获得Recycler中缓存的View
                View itemView = recycler.getViewForPosition(i);
                measureChildWithMargins(itemView, 0, 0);
                //添加View到RecyclerView上
                addView(itemView);
                //取出先前存好的ItemView的位置矩形
                Rect rect = allItems.get(i);
                //将这个item布局出来
                layoutDecoratedWithMargins(itemView,
                        rect.left,
                        rect.top - verticalScrollOffset,  //因为现在是复用View,所以想要显示在
                        rect.right,
                        rect.bottom - verticalScrollOffset);
            }
        }
        //展示实际渲染的 item 的个数
        Log.e("lxy", "item count = " + getChildCount());
    }

这时就基本实现了回收,再考虑下滑动的情况。

    int verticalScrollOffset = 0;
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int distance = dy;
        // recycleAndFillView 中循环判断时,可能重复添加同一个item,所以addview前要先detach
        detachAndScrapAttachedViews(recycler);
        if (verticalScrollOffset + dy < 0) {
            distance = -verticalScrollOffset;
        } else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {
            distance = totalHeight - getVerticalSpace() - verticalScrollOffset;
        }

        //将竖直方向的偏移量+distance
        verticalScrollOffset += distance;

        // 调用该方法通知view在y方向上移动指定距离
        offsetChildrenVertical(-distance);
        recycleAndFillView(recycler, state);
        return distance;
    }

PS:滑动基于offsetChildrenVertical,也就是offsetXXXAndXXX方法,其他的博客说方法不会触发重新绘制,是基于RenderNode实现的。

offsetTopAndBottom方法设置View的位置, 不会造成View的重绘,代码里可以看见通过RenderNode.offsetTopAndBottom来实现的。(Draw绘制完的东西保存在RenderNode里面,所以位置的修改可以直接修改RenderNode)

参考

https://blog.csdn.net/huachao1001/article/details/51594004#rd

https://www.jianshu.com/p/7bb7556bbe10

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0517/2880.html

猜你喜欢

转载自blog.csdn.net/FeiLaughing/article/details/83577543
今日推荐