android开发实现RecyclerView左右上下流式布局FlowLayoutManager

  1. 效果图如下,左右滑动流式布局
    image

  2. 直接上代码,如下:


import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;

import java.util.stream.Stream;

/**
 * @author:yongfeng
 * @data:2025/9/28 17:58
 */
public class FlowLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider {
    static final int LAYOUT_START = -1;
    static final int LAYOUT_END = 1;
    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;
    public static final int INVALID_OFFSET = Integer.MIN_VALUE;
    private static final int DEFAULT_HORIZONTAL_SPACE = 12;
    private static final int DEFAULT_VERTICAL_SPACE = 12;
    private SparseArray<Rect> mScrapRects;
    private SparseIntArray mColumnCountOfRow;
    private SparseArray<LayoutParams> mScrapSites;
    private int mOffsetX;
    private int mOffsetY;
    private int mItemCount;
    private int mLeft;
    private int mTop;
    private int mRight;
    private int mBottom;
    private int mWidth;
    private int mHeight;
    private int mTotalWidth;
    private int mTotalHeight;
    private int mScrollOffsetX;
    private int mScrollOffsetY;
    private int mVerticalSpace;
    private int mHorizontalSpace;
    int mPendingScrollPositionOffset;
    @FlowLayoutManager.Orientation
    private int mOrientation;
    private RecyclerView.Recycler mRecycler;
    private RecyclerView.State mState;

    public FlowLayoutManager() {
        this(1);
    }

    public FlowLayoutManager(@FlowLayoutManager.Orientation int orientation) {
        this(orientation, 12, 12);
    }

    public FlowLayoutManager(@FlowLayoutManager.Orientation int orientation, int verticalSpace, int horizontalSpace) {
        this.mVerticalSpace = 12;
        this.mHorizontalSpace = 12;
        this.mPendingScrollPositionOffset = Integer.MIN_VALUE;
        this.mOrientation = 1;
        this.setOrientation(orientation);
        this.setSpace(verticalSpace, horizontalSpace);
        this.setAutoMeasureEnabled(true);
    }

    public FlowLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this.mVerticalSpace = 12;
        this.mHorizontalSpace = 12;
        this.mPendingScrollPositionOffset = Integer.MIN_VALUE;
        this.mOrientation = 1;
        RecyclerView.LayoutManager.Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
        this.setOrientation(properties.orientation);
        this.setReverseLayout(properties.reverseLayout);
        this.setStackFromEnd(properties.stackFromEnd);
        this.setAutoMeasureEnabled(true);
    }

    public void setSpace(int verticalSpace, int horizontalSpace) {
        if (verticalSpace != this.mVerticalSpace || horizontalSpace != this.mHorizontalSpace) {
            this.mVerticalSpace = verticalSpace;
            this.mHorizontalSpace = horizontalSpace;
            this.requestLayout();
        }
    }

    public void setOrientation(@FlowLayoutManager.Orientation int orientation) {
        if (orientation != this.mOrientation) {
            this.mOrientation = orientation;
            this.requestLayout();
        }
    }

    public void setReverseLayout(boolean reverseLayout) {
    }

    public void setStackFromEnd(boolean stackFromEnd) {
    }

    @FlowLayoutManager.Orientation
    public int getOrientation() {
        return this.mOrientation;
    }

    public LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(-2, -2);
    }

    public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
        return new LayoutParams(c, attrs);
    }

    public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return lp instanceof ViewGroup.MarginLayoutParams ? new LayoutParams((ViewGroup.MarginLayoutParams)lp) : new LayoutParams(lp);
    }

    public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
        return lp instanceof LayoutParams;
    }

    public boolean supportsPredictiveItemAnimations() {
        return true;
    }

    public void scrollToPosition(int position) {
        if (position < this.getItemCount()) {
            View view = this.findViewByPosition(position);
            if (view != null) {
                if (this.canScrollVertically()) {
                    this.scrollVerticallyBy((int)(view.getY() - (float)this.mTop), this.mRecycler, this.mState);
                } else if (this.canScrollHorizontally()) {
                    this.scrollHorizontallyBy((int)(view.getX() - (float)this.mLeft), this.mRecycler, this.mState);
                }
            }

        }
    }

    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext());
        scroller.setTargetPosition(position);
        this.startSmoothScroll(scroller);
    }

    public PointF computeScrollVectorForPosition(int targetPosition) {
        int direction = this.calculateScrollDirectionForPosition(targetPosition);
        PointF outVector = new PointF();
        if (direction == 0) {
            return null;
        } else {
            if (this.canScrollHorizontally()) {
                outVector.x = (float)direction;
                outVector.y = 0.0F;
            } else if (this.canScrollVertically()) {
                outVector.x = 0.0F;
                outVector.y = (float)direction;
            }

            return outVector;
        }
    }

    public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
        this.mPendingScrollPositionOffset = Integer.MIN_VALUE;
        this.removeAllViews();
    }

    public void onLayoutCompleted(RecyclerView.State state) {
    }

    @RequiresApi(
            api = 24
    )
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        this.mRecycler = recycler;
        this.mState = state;
        this.mItemCount = this.getItemCount();
        if (this.mItemCount == 0) {
            this.detachAndScrapAttachedViews(recycler);
        } else if (this.getChildCount() != 0 || !state.isPreLayout()) {
            this.mScrapRects = new SparseArray(this.mItemCount);
            this.mScrapSites = new SparseArray(this.mItemCount);
            this.mColumnCountOfRow = new SparseIntArray();
            this.mScrollOffsetX = 0;
            this.mScrollOffsetY = 0;
            this.mWidth = this.getWidth();
            this.mHeight = this.getHeight();
            this.mLeft = this.getPaddingLeft();
            this.mTop = this.getPaddingTop();
            this.mRight = this.getPaddingRight();
            this.mBottom = this.getPaddingBottom();
            this.mOffsetX = this.mLeft;
            this.mOffsetY = this.mTop;
            this.detachAndScrapAttachedViews(recycler);
            if (this.canScrollVertically()) {
                this.mTotalHeight = this.calculateVerticalChildrenSites(recycler);
                this.scrollVerticallyBy(this.mPendingScrollPositionOffset, recycler, state);
            } else {
                this.mTotalWidth = this.calculateHorizontalChildrenSites(recycler);
                this.scrollHorizontallyBy(this.mPendingScrollPositionOffset, recycler, state);
            }

        }
    }

    @RequiresApi(
            api = 24
    )
    private int calculateVerticalChildrenSites(RecyclerView.Recycler recycler) {
        int[] maxRowHeight = new int[]{0};
        int[] totalHeight = new int[]{0};
        Point point = new Point();
        Stream.iterate(0, (index) -> {
            return index + 1;
        }).limit((long)this.mItemCount).forEach((position) -> {
            View scrap = recycler.getViewForPosition(position);
            this.addView(scrap);
            this.measureChildWithMargins(scrap, 0, 0);
            int width = this.getDecoratedMeasurementHorizontal(scrap);
            int height = this.getDecoratedMeasurementVertical(scrap);
            if (this.mOffsetX + width + this.mHorizontalSpace > this.mWidth - this.mRight) {
                this.mOffsetX = this.mLeft;
                this.mOffsetY += maxRowHeight[0] + (position == 0 ? 0 : this.mVerticalSpace);
                maxRowHeight[0] = 0;
                point.x = 0;
                ++point.y;
            }

            maxRowHeight[0] = Math.max(height, maxRowHeight[0]);
            LayoutParams lp = (LayoutParams)scrap.getLayoutParams();
            lp.column = point.x++;
            lp.row = point.y;
            if (lp.column != 0) {
                this.mOffsetX += this.mHorizontalSpace;
            }

            this.mScrapSites.put(position, lp);
            this.mColumnCountOfRow.put(lp.row, lp.column + 1);
            Rect frame = (Rect)this.mScrapRects.get(position);
            if (frame == null) {
                frame = new Rect();
            }

            frame.set(this.mOffsetX, this.mOffsetY, this.mOffsetX += width, this.mOffsetY + height);
            this.mScrapRects.put(position, frame);
            totalHeight[0] = Math.max(totalHeight[0], this.mOffsetY + height);
            this.layoutDecoratedWithMargins(scrap, frame.left, frame.top, frame.right, frame.bottom);
        });
        return Math.max(totalHeight[0] - this.mTop, this.getVerticalSpace());
    }

    @SuppressLint({"NewApi"})
    private int calculateHorizontalChildrenSites(RecyclerView.Recycler recycler) {
        int[] maxColumnWidth = new int[]{0};
        int[] totalWidth = new int[]{0};
        Point point = new Point();
        Stream.iterate(0, (index) -> {
            return index + 1;
        }).limit((long)this.mItemCount).forEach((position) -> {
            View scrap = recycler.getViewForPosition(position);
            this.addView(scrap);
            this.measureChildWithMargins(scrap, 0, 0);
            int width = this.getDecoratedMeasurementHorizontal(scrap);
            int height = this.getDecoratedMeasurementVertical(scrap);
            if (this.mOffsetY + height + this.mVerticalSpace > this.mHeight - this.mBottom) {
                this.mOffsetY = this.mTop;
                this.mOffsetX += maxColumnWidth[0] + (position == 0 ? 0 : this.mHorizontalSpace);
                maxColumnWidth[0] = 0;
                ++point.x;
                point.y = 0;
            }

            maxColumnWidth[0] = Math.max(width, maxColumnWidth[0]);
            LayoutParams lp = (LayoutParams)scrap.getLayoutParams();
            lp.column = point.x;
            lp.row = point.y++;
            if (lp.row != 0) {
                this.mOffsetY += this.mVerticalSpace;
            }

            this.mScrapSites.put(position, lp);
            this.mColumnCountOfRow.put(lp.row, lp.column + 1);
            Rect frame = (Rect)this.mScrapRects.get(position);
            if (frame == null) {
                frame = new Rect();
            }

            frame.set(this.mOffsetX, this.mOffsetY, this.mOffsetX + width, this.mOffsetY += height);
            this.mScrapRects.put(position, frame);
            totalWidth[0] = Math.max(totalWidth[0], this.mOffsetX + width);
            this.layoutDecoratedWithMargins(scrap, frame.left, frame.top, frame.right, frame.bottom);
        });
        return Math.max(totalWidth[0] - this.mLeft, this.getHorizontalSpace());
    }

    @RequiresApi(
            api = 24
    )
    private void fillAndRecycleView(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (this.mItemCount != 0 && !state.isPreLayout()) {
            Rect displayFrame = this.canScrollVertically() ? new Rect(this.mLeft, this.mScrollOffsetY, this.mWidth, this.mScrollOffsetY + this.mHeight) : new Rect(this.mScrollOffsetX, this.mTop, this.mScrollOffsetX + this.mWidth, this.mHeight);
            Stream.iterate(0, (index) -> {
                return index + 1;
            }).limit((long)this.mItemCount).forEach((index) -> {
                Rect rect = (Rect)this.mScrapRects.get(index);
                View scrap;
                if (!Rect.intersects(displayFrame, rect)) {
                    scrap = this.getChildAt(index);
                    if (scrap != null) {
                        this.removeAndRecycleView(scrap, recycler);
                    }

                } else {
                    scrap = recycler.getViewForPosition(index);
                    this.addView(scrap);
                    this.measureChildWithMargins(scrap, 0, 0);
                    if (this.canScrollVertically()) {
                        this.layoutDecoratedWithMargins(scrap, rect.left, rect.top - this.mScrollOffsetY, rect.right, rect.bottom - this.mScrollOffsetY);
                    } else {
                        this.layoutDecoratedWithMargins(scrap, rect.left - this.mScrollOffsetX, rect.top, rect.right - this.mScrollOffsetX, rect.bottom);
                    }

                }
            });
        }
    }

    public boolean canScrollVertically() {
        return this.mOrientation == 1;
    }

    public boolean canScrollHorizontally() {
        return this.mOrientation == 0;
    }

    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (dy != 0 && this.mItemCount != 0) {
            int travel = dy;
            if (this.mScrollOffsetY + travel < 0) {
                travel = -this.mScrollOffsetY;
            } else if (this.mScrollOffsetY + travel > this.mTotalHeight - this.getVerticalSpace()) {
                travel = this.mTotalHeight - this.getVerticalSpace() - this.mScrollOffsetY;
            }

            this.mPendingScrollPositionOffset = this.mScrollOffsetY += travel;
            this.offsetChildrenVertical(-travel);
            return travel;
        } else {
            return 0;
        }
    }

    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (dx != 0 && this.mItemCount != 0) {
            int travel = dx;
            if (this.mScrollOffsetX + travel < 0) {
                travel = -this.mScrollOffsetX;
            } else if (this.mScrollOffsetX + travel > this.mTotalWidth - this.getHorizontalSpace()) {
                travel = this.mTotalWidth - this.getHorizontalSpace() - this.mScrollOffsetX;
            }

            this.mPendingScrollPositionOffset = this.mScrollOffsetX += travel;
            this.offsetChildrenHorizontal(-travel);
            return travel;
        } else {
            return 0;
        }
    }

    @Nullable
    public LayoutParams getLayoutParamsByPosition(int position) {
        return (LayoutParams)this.mScrapSites.get(position);
    }

    public int getRow(int position) {
        LayoutParams params = this.getLayoutParamsByPosition(position);
        return params != null ? params.row : 0;
    }

    public int getColumn(int position) {
        LayoutParams params = this.getLayoutParamsByPosition(position);
        return params != null ? params.column : 0;
    }

    public int getColumnCountOfRow(int row) {
        return this.mColumnCountOfRow.get(row, 1);
    }

    private int calculateScrollDirectionForPosition(int position) {
        if (this.getChildCount() == 0) {
            return -1;
        } else {
            return position < this.getFirstChildPosition() ? -1 : 1;
        }
    }

    int getFirstChildPosition() {
        int childCount = this.getChildCount();
        return childCount == 0 ? 0 : this.getPosition(this.getChildAt(0));
    }

    private int getDecoratedMeasurementHorizontal(View view) {
        LayoutParams params = (LayoutParams)view.getLayoutParams();
        return this.getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin;
    }

    private int getDecoratedMeasurementVertical(View view) {
        LayoutParams params = (LayoutParams)view.getLayoutParams();
        return this.getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
    }

    private int getVerticalSpace() {
        return this.mHeight - this.mBottom - this.mTop;
    }

    private int getHorizontalSpace() {
        return this.mWidth - this.mLeft - this.mRight;
    }

    public static class LayoutParams extends RecyclerView.LayoutParams {
        public int row;
        public int column;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        public LayoutParams(RecyclerView.LayoutParams source) {
            super(source);
        }

        public String toString() {
            return "LayoutParams = {width=" + this.width + ",height=" + this.height + ",row=" + this.row + ",column=" + this.column + "}";
        }
    }

    public @interface Orientation {
    }
}

posted @ 2025-10-10 17:42  yongfengnice  阅读(7)  评论(0)    收藏  举报