RecyclerView自定义LayoutManager 之流式布局 FlowLaoutManager

746 阅读3分钟

需求

今天遇到了一个需求,要完成一个具有流式布局的列表。 一看到这个需求,心里一凉,因为要用到RecyclerView的自定义LayoutManager。而我对着不太熟悉。临阵磨枪,在网络上找了好多教程学了一天。终于让我给弄好了。

主要跟着下面两个教程完成自定义LayoutManager的学习。第一篇对RecyclerView各部分组成、原理,特别是缓存方面说得很清晰。第二篇则在实践方面说得很清晰。

# 自定义 LayoutManager,让 RecyclerView 效果起飞
# 看完这篇文章你还不会自定义LayoutManager,我吃X (聪明如我,让这位大佬少吃一次X)

实现结果

  1. 两种效果,一种会在换行时将上一行铺满一行
  2. 利用了Recycler对View进行缓存和复用,自测Adapter中传入大小为十万的列表,还可以顺畅滑动。
  3. 边缘检测与限制

存在问题

  1. 上滑时的填充逻辑有待完善,下滑后再上滑,显示的布局会不一样
  2. 快速滑动时,布局会错乱

源码如下

有空再解释解释吧,最近在追《绝命毒师》,好精彩。煲剧去了。

import android.util.Log;
import android.view.View;

import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;


public class FlowLayoutManager extends RecyclerView.LayoutManager {

    private static final String TAG = "FlowLayoutManager";

    // 是否开启铺平整行的功能
    private boolean isOpenFlatLine = false;


    public FlowLayoutManager(boolean isOpenFlatLine) {
        this.isOpenFlatLine = isOpenFlatLine;
    }

    @Override
    public boolean isAutoMeasureEnabled() {
        return true;
    }

    @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);

        int beginIndex = 0;
        int fixOffset = 0;
        // 非第一次调用onLayoutChild时,设置开始元素和垂直修正偏移
        if (getChildCount() > 0) {
            View childAt = getChildAt(0);
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) childAt.getLayoutParams();
            beginIndex = getPosition(childAt);
            fixOffset = getDecoratedTop(childAt) - layoutParams.topMargin;
            Log.i(TAG, "childCount = " + getChildCount() + "  beginIndex=" + beginIndex + "  fixOffset=" + fixOffset);
        }


        //这里是在布局前将所有在显示的HoldView从RecyclerView中剥离,将其放在缓存中,方便重新布局时使用
        detachAndScrapAttachedViews(recycler);

        // 剩余高度足够才会填充View
        int totalHeight = getHeight();

        //curLineWidth 累加item布局时的x轴偏移
        int curLineWidth = 0;
        //curLineTop 累加item布局时的y轴偏移
        int curLineTop = 0;
        int lastLineMaxHeight = 0;

        View lastView = null;
        for (int i = beginIndex; i < getItemCount() && totalHeight > 0; i++) {
            View view = recycler.getViewForPosition(i);
            //获取每个item的布局参数,计算每个item的占用位置时需要加上margin
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
            // 添加每一个View
            addView(view);
            // 测量View的宽高 (支持margin)
            measureChildWithMargins(view, 0, 0);
            int width = getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin;
            int height = getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
            //累加当前行已有item的宽度
            curLineWidth += width;
            if (curLineWidth <= getWidth()) {
                //如果累加的宽度小于等于RecyclerView的宽度,不需要换行
                layoutDecorated(view,
                        curLineWidth - width + params.leftMargin, // 前一个Right + leftMargin
                        curLineTop + params.topMargin,  // 上一行bottom + bottomMargin + topMargin
                        curLineWidth - params.rightMargin, // 前一个Right + leftMargin + width
                        curLineTop + height - params.bottomMargin); // 上一行bottom + height - topMargin
                lastLineMaxHeight = Math.max(lastLineMaxHeight, height);
            } else {
                if (lastView != null) {
                    flatLineView(lastView);
                }
                totalHeight -= height;
                // 换行
                curLineWidth = width;
                if (lastLineMaxHeight == 0) {
                    lastLineMaxHeight = height;
                }
                //记录当前行top
                curLineTop += lastLineMaxHeight;
                layoutDecorated(
                        view,
                        params.leftMargin,
                        curLineTop + params.topMargin,
                        width - params.rightMargin,
                        curLineTop + height - params.bottomMargin
                );
                lastLineMaxHeight = height;
            }
            lastView = view;
        }
        offsetChildrenVertical(fixOffset);
    }


    @Override
    public boolean canScrollVertically() {
        return true;
    }


    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        // dy > 0 --> 手指往上滑, 需要填充下方元素
        // dy < 0 --> 手指往下滑, 需要填充上方的元素

        // 1. 填充子View
        int consume = fill(dy, recycler);

        // 2. 滚动子View
        offsetChildrenVertical(-consume);

        // 3. 回收子View
        recycler(consume, recycler);


        return dy;
    }


    private int fill(int dy, RecyclerView.Recycler recycler) {
        if (dy == 0 || getItemCount() == 0) {
            return 0;
        }


        if (dy > 0) {
            // 填充尾部内容
            while (true) {
                // 1. 找到锚点View
                int childCount = getChildCount();
                View anchorView = getChildAt(childCount - 1);
                int decoratedRight = getDecoratedRight(anchorView);
                int decoratedTop = getDecoratedTop(anchorView);
                int decoratedBottom = getDecoratedBottom(anchorView);
                int adapterPosition = getPosition(anchorView);
                RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) anchorView.getLayoutParams();
                int anchorViewRightMargin = layoutParams.rightMargin;
                int anchorViewBottomMargin = layoutParams.bottomMargin;

                int fillPosition = adapterPosition + 1; // 被填充的Item在Adapter中的位置。

                // 边缘检测
                if (fillPosition >= getItemCount()) {
                    if (decoratedBottom + anchorViewBottomMargin - dy < getHeight()) {
                        // 如果View的底线超过了decoratedBottom + anchorViewBottomMargin, 就修正具体滑动的距离。
                        return decoratedBottom + anchorViewBottomMargin - getHeight();
                    }
                    return dy;
                }

                // 2. addView
                View viewForPosition = recycler.getViewForPosition(fillPosition);
                addView(viewForPosition);
                RecyclerView.LayoutParams insertViewParam = (RecyclerView.LayoutParams) viewForPosition.getLayoutParams();


                // 3. 计算View的大小
                measureChildWithMargins(viewForPosition, 0, 0);
                int width = getDecoratedMeasuredWidth(viewForPosition);
                int height = getDecoratedMeasuredHeight(viewForPosition);

                // 一直添加View直到View超出了屏幕范围
                int topMargin = insertViewParam.topMargin;
                int leftMargin = insertViewParam.leftMargin;
                int rightMargin = insertViewParam.rightMargin;

                if (decoratedRight + width > getWidth() && decoratedBottom + topMargin > getHeight()) {
                    removeAndRecycleView(viewForPosition, recycler);
                    break;
                }

                // 4. layoutView
                if (decoratedRight + anchorViewRightMargin + width + leftMargin <= getWidth()) {
                    // 不用换行
                    layoutDecorated(viewForPosition,
                            decoratedRight + anchorViewRightMargin + leftMargin,
                            decoratedTop,
                            decoratedRight + anchorViewRightMargin + leftMargin + width,
                            decoratedTop + height);
                } else {
                    flatLineView(anchorView);
                    // 需要换行
                    layoutDecorated(viewForPosition,
                            leftMargin,
                            decoratedBottom + anchorViewBottomMargin + topMargin,
                            leftMargin + width,
                            decoratedBottom + anchorViewBottomMargin + topMargin + height);
                }
            }
        } else {
            // 填充头部元素
            while (true) {
                // 1. 找到锚点元素
                View anchorView = getChildAt(0);
                int anchorPosition = getPosition(anchorView);
                RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) anchorView.getLayoutParams();
                int anchorTopMargin = layoutParams.topMargin;
                int decoratedTop = getDecoratedTop(anchorView);
                int fillPosition = anchorPosition - 1;

                // 边缘检测
                if (fillPosition < 0) {
                    if (decoratedTop - anchorTopMargin - dy > 0) {
                        return decoratedTop - anchorTopMargin;
                    }
                    return dy;
                }

                // 2. 从Recycler获取目标View,
                View viewForPosition = recycler.getViewForPosition(fillPosition);
                RecyclerView.LayoutParams targetViewParam = (RecyclerView.LayoutParams) viewForPosition.getLayoutParams();
                int leftMargin = targetViewParam.leftMargin;
                int rightMargin = targetViewParam.rightMargin;
                int bottomMargin = targetViewParam.bottomMargin;

                // 3. 测量View的大小
                addView(viewForPosition, 0);
                measureChildWithMargins(viewForPosition, 0, 0);
                int height = getDecoratedMeasuredHeight(viewForPosition);
                int width = getDecoratedMeasuredWidth(viewForPosition);


                // 4. 查看锚点元素那一行还有没有空位,如果有,则插入到那一行的第一个。如果没有则插入到上一行的第一个
                ArrayList<View> currentLineViews = getCurrentLineView(anchorView);
                Log.i(TAG, currentLineViews.toString());
                View lastView = currentLineViews.get(currentLineViews.size() - 1);
                int rightestInLine = getDecoratedRight(lastView);

                if (rightestInLine + width + leftMargin + rightMargin <= getWidth()) {
                    // 插入当前行的第一个。
                    layoutDecorated(viewForPosition,
                            leftMargin,
                            decoratedTop,
                            leftMargin + width,
                            decoratedTop + height);

                    int lineStart = leftMargin + width + rightMargin;
                    // 同一行的后续Item,重新layout
                    for (int i = 0; i < currentLineViews.size(); i++) {
                        View curLine = currentLineViews.get(i);
                        RecyclerView.LayoutParams layoutParam = (RecyclerView.LayoutParams) curLine.getLayoutParams();
                        int viewHeight = getDecoratedMeasuredHeight(curLine);
                        int viewWidth = getDecoratedMeasuredWidth(curLine);

                        layoutDecorated(curLine,
                                lineStart + layoutParam.leftMargin,
                                decoratedTop,
                                lineStart + layoutParam.leftMargin + viewWidth,
                                decoratedTop + viewHeight
                        );

                        lineStart += layoutParam.leftMargin + viewWidth + layoutParam.rightMargin;
                    }
                } else {
                    // 插入上一行的第一个

                    // 如果超出范围,则不添加
                    if (decoratedTop - anchorTopMargin - bottomMargin < 0) {
                        removeAndRecycleView(viewForPosition, recycler);
                        break;
                    }

                    flatLineView(anchorView);

                    layoutDecorated(viewForPosition,
                            leftMargin,
                            decoratedTop - anchorTopMargin - bottomMargin - height,
                            leftMargin + width,
                            decoratedTop - anchorTopMargin - bottomMargin
                    );
                }
            }
        }
        return dy;
    }

    /**
     * @param anchorView 返回与anchorView同一行的View
     * @return
     */
    private ArrayList<View> getCurrentLineView(View anchorView) {
        int anchorTop = getDecoratedTop(anchorView);
        ArrayList<View> res = new ArrayList<>();
        for (int i = 0; i < getChildCount(); i++) {
            View childAt = getChildAt(i);
            int decoratedTop = getDecoratedTop(childAt);
            if (decoratedTop == anchorTop) {
                res.add(childAt);
            }
        }
        return res;
    }

    /**
     * 把超出范围的回收掉
     *
     * @param dy
     * @param recycler
     */
    private void recycler(int dy, RecyclerView.Recycler recycler) {
        if (dy > 0) {
            // 向上滑,回收顶部元素
            for (int i = 0; i < getChildCount(); i++) {
                View childAt = getChildAt(i);
                // 获取该元素的底部位置,如果
                if (childAt == null) {
                    continue;
                }
                int decoratedBottom = getDecoratedBottom(childAt);

                // 从第一个元素遍历,如果遇到某个bottom大于0的元素,说明后续元素都是可见的。
                if (decoratedBottom > 0) {
                    break;
                }
                removeAndRecycleView(childAt, recycler);
            }
        } else {
            // 向下滑,回收尾部元素
            for (int i = getChildCount() - 1; i > 0; i--) {
                View childAt = getChildAt(i);
                if (childAt == null) {
                    continue;
                }

                int decoratedTop = getDecoratedTop(childAt);
                if (decoratedTop < getHeight()) {
                    break;
                }
                removeAndRecycleView(childAt, recycler);
            }
        }
    }

    /**
     * 将anchorView所在的行中的每个View扩宽,以填满整行
     *
     * @param anchorView
     */
    private void flatLineView(View anchorView) {
        if (!isOpenFlatLine) {
            return;
        }

        ArrayList<View> currentLineViews = getCurrentLineView(anchorView);
        View lastViewInLine = currentLineViews.get(currentLineViews.size() - 1);
        RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) lastViewInLine.getLayoutParams();
        int decoratedRight = getDecoratedRight(lastViewInLine);
        int decoratedTop = getDecoratedTop(lastViewInLine);
        // 这一行右边的空位
        int dis = getWidth() - decoratedRight - layoutParams.rightMargin;
        int disPerView = dis / currentLineViews.size();

        int lineStart = 0;
        for (int i = 0; i < currentLineViews.size(); i++) {
            View curView = currentLineViews.get(i);
            RecyclerView.LayoutParams curParam = (RecyclerView.LayoutParams) curView.getLayoutParams();
            int width = getDecoratedMeasuredWidth(curView);
            int height = getDecoratedMeasuredHeight(curView);
            layoutDecorated(curView,
                    lineStart + curParam.leftMargin,
                    decoratedTop,
                    lineStart + curParam.leftMargin + width + disPerView,
                    decoratedTop + height);
            lineStart += curParam.leftMargin + width + curParam.rightMargin + disPerView;
        }
    }
}