Browse Source

feat: 流式布局问题修复

DoggyZhang 1 tháng trước cách đây
mục cha
commit
06d84838c0

+ 908 - 0
app/src/main/java/com/adealink/weparty/commonui/recycleview/layoutmanager/FlowLayoutManager.java

@@ -0,0 +1,908 @@
+package com.adealink.weparty.commonui.recycleview.layoutmanager;
+
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.GravityCompat;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.OrientationHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+
+/**
+ * Created by Astrocode on 26.05.18.
+ */
+public class FlowLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider {
+
+    public final static int VERTICAL = OrientationHelper.VERTICAL, HORIZONTAL = OrientationHelper.HORIZONTAL;
+    public final static int DEFAULT_COUNT_ITEM_IN_LINE = -1;
+
+    private final static String TAG_FIRST_ITEM_ADAPTER_INDEX = "TAG_FIRST_ITEM_ADAPTER_INDEX";
+    private final static String TAG_FIRST_LINE_START_POSITION = "TAG_FIRST_LINE_START_POSITION";
+
+    private final static String ERROR_UNKNOWN_ORIENTATION = "Unknown orientation!";
+    private final static String ERROR_BAD_ARGUMENT = "Inappropriate field value!";
+
+    private int mGravity;
+    private int mOrientation;
+
+    private int mMaxItemsInLine;
+
+    private int mSpacingBetweenItems;
+    private int mSpacingBetweenLines;
+
+    private FLMLayoutManagerHelper mLayoutManagerHelper;
+
+    private ArrayList<Line> mCurrentLines;
+
+    private int mFirstItemAdapterIndex;
+    private int mFirstLineStartPosition;
+
+    public FlowLayoutManager(int orientation) {
+        this(orientation, Gravity.START, DEFAULT_COUNT_ITEM_IN_LINE, 0, 0);
+    }
+
+    public FlowLayoutManager(int orientation, int gravity) {
+        this(orientation, gravity, DEFAULT_COUNT_ITEM_IN_LINE, 0, 0);
+    }
+
+    public FlowLayoutManager(int orientation, int gravity, int spacingBetweenItems, int spacingBetweenLines) {
+        this(orientation, gravity, DEFAULT_COUNT_ITEM_IN_LINE, spacingBetweenItems, spacingBetweenLines);
+    }
+
+    public FlowLayoutManager(int orientation, int gravity, int maxItemsInLine, int spacingBetweenItems, int spacingBetweenLines) {
+        mCurrentLines = new ArrayList<>();
+
+        mGravity = gravity;
+
+        mFirstItemAdapterIndex = 0;
+        mFirstLineStartPosition = -1;
+
+        if (maxItemsInLine == 0 || maxItemsInLine < -1) {
+            throw new IllegalArgumentException(ERROR_BAD_ARGUMENT);
+        }
+
+        mMaxItemsInLine = maxItemsInLine;
+
+        if (mSpacingBetweenItems < 0) {
+            throw new IllegalArgumentException(ERROR_BAD_ARGUMENT);
+        }
+
+        mSpacingBetweenItems = spacingBetweenItems;
+
+        if (mSpacingBetweenLines < 0) {
+            throw new IllegalArgumentException(ERROR_BAD_ARGUMENT);
+        }
+
+        mSpacingBetweenLines = spacingBetweenLines;
+
+        if (orientation != HORIZONTAL && orientation != VERTICAL) {
+            throw new IllegalArgumentException(ERROR_UNKNOWN_ORIENTATION);
+        }
+        mOrientation = orientation;
+
+        mLayoutManagerHelper = FLMLayoutManagerHelper.createLayoutManagerHelper(this, orientation, mGravity);
+    }
+
+    @Override
+    public boolean isAutoMeasureEnabled() {
+        return true;
+    }
+
+    @Override
+    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+        Line currentLine = null;
+
+        if (mFirstLineStartPosition == -1) {
+            mFirstLineStartPosition = mLayoutManagerHelper.getStartAfterPadding();
+        }
+
+        int topOrLeft = mFirstLineStartPosition;
+
+        detachAndScrapAttachedViews(recycler);
+        mCurrentLines.clear();
+
+        for (int i = mFirstItemAdapterIndex; i < getItemCount(); i += currentLine.mItemsCount) {
+            currentLine = addLineToEnd(i, topOrLeft, recycler);
+
+            mCurrentLines.add(currentLine);
+
+            topOrLeft = mSpacingBetweenLines + currentLine.mEndValueOfTheHighestItem;
+
+            if (currentLine.mEndValueOfTheHighestItem > mLayoutManagerHelper.getEndAfterPadding()) {
+                break;
+            }
+        }
+
+        if (mFirstItemAdapterIndex > 0 && currentLine != null) {
+            int availableOffset = currentLine.mEndValueOfTheHighestItem - mLayoutManagerHelper.getEnd() + mLayoutManagerHelper.getEndPadding();
+
+            if (availableOffset < 0) {
+                if (mOrientation == VERTICAL) {
+                    scrollVerticallyBy(availableOffset, recycler, state);
+                } else {
+                    scrollHorizontallyBy(availableOffset, recycler, state);
+                }
+            }
+        }
+
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        Bundle data = (Bundle) state;
+
+        mFirstItemAdapterIndex = data.getInt(TAG_FIRST_ITEM_ADAPTER_INDEX);
+        mFirstLineStartPosition = data.getInt(TAG_FIRST_LINE_START_POSITION);
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Bundle data = new Bundle();
+
+        data.putInt(TAG_FIRST_ITEM_ADAPTER_INDEX, mFirstItemAdapterIndex);
+        data.putInt(TAG_FIRST_LINE_START_POSITION, mFirstLineStartPosition);
+
+        return data;
+    }
+
+    /**
+     * Change orientation of the layout manager
+     *
+     * @param orientation New orientation.
+     */
+
+    public void setOrientation(int orientation) {
+
+        if (orientation != HORIZONTAL && orientation != VERTICAL) {
+            throw new IllegalArgumentException(ERROR_UNKNOWN_ORIENTATION);
+        }
+
+        if (orientation != mOrientation) {
+            assertNotInLayoutOrScroll(null);
+
+            mOrientation = orientation;
+            mLayoutManagerHelper = FLMLayoutManagerHelper.createLayoutManagerHelper(this, orientation, mGravity);
+
+            requestLayout();
+        }
+    }
+
+    public void setMaxItemsInLine(int maxItemsInLine) {
+
+        if (maxItemsInLine <= 0) {
+            throw new IllegalArgumentException(ERROR_BAD_ARGUMENT);
+        }
+
+        assertNotInLayoutOrScroll(null);
+
+        mMaxItemsInLine = maxItemsInLine;
+
+        requestLayout();
+    }
+
+    public void setSpacingBetweenItems(int spacingBetweenItems) {
+
+        if (spacingBetweenItems < 0) {
+            throw new IllegalArgumentException(ERROR_BAD_ARGUMENT);
+        }
+
+        assertNotInLayoutOrScroll(null);
+
+        mSpacingBetweenItems = spacingBetweenItems;
+
+        requestLayout();
+    }
+
+    public void setSpacingBetweenLines(int spacingBetweenLines) {
+
+        if (spacingBetweenLines < 0) {
+            throw new IllegalArgumentException(ERROR_BAD_ARGUMENT);
+        }
+
+        assertNotInLayoutOrScroll(null);
+
+        mSpacingBetweenLines = spacingBetweenLines;
+
+        requestLayout();
+    }
+
+    /**
+     * Return current orientation of the layout manager.
+     */
+    public int getOrientation() {
+        return mOrientation;
+    }
+
+    /**
+     * Change gravity of the layout manager.
+     *
+     * @param gravity New gravity.
+     */
+    public void setGravity(int gravity) {
+        assertNotInLayoutOrScroll(null);
+
+        if (gravity != mGravity) {
+            mGravity = gravity;
+            mLayoutManagerHelper.setGravity(gravity);
+
+            requestLayout();
+        }
+    }
+
+    /**
+     * Return current gravity of the layout manager.
+     */
+    public int getGravity() {
+        return mGravity;
+    }
+
+    /**
+     * Add one line to the end of recyclerView.
+     *
+     * @param startAdapterIndex Adapter index of first item of new line.
+     * @param start             Start position(Top - if orientation is VERTICAL or Left - if orientation is HORIZONTAL) of the new line.
+     * @return New line.
+     */
+    @NonNull
+    private Line addLineToEnd(int startAdapterIndex, int start, RecyclerView.Recycler recycler) {
+        boolean isEndOfLine = false;
+
+        int currentAdapterIndex = startAdapterIndex;
+        int currentItemsSize = 0;
+        int currentMaxValue = 0;
+
+        Line line = new Line();
+        line.mStartValueOfTheHighestItem = start;
+
+        while (!isEndOfLine && currentAdapterIndex < getItemCount()) {
+            final View view = recycler.getViewForPosition(currentAdapterIndex);
+
+            addView(view);
+
+            measureChildWithMargins(view, 0, 0);
+
+            final int widthOrHeight = mLayoutManagerHelper.getDecoratedMeasurementInOther(view);
+            final int heightOrWidth = mLayoutManagerHelper.getDecoratedMeasurement(view);
+
+            if (line.mItemsCount == mMaxItemsInLine || (currentItemsSize + widthOrHeight) >= mLayoutManagerHelper.getLineSize()) {
+                isEndOfLine = true;
+
+                if (currentItemsSize == 0) {
+                    currentMaxValue = heightOrWidth;
+
+                    line.mEndValueOfTheHighestItem = line.mStartValueOfTheHighestItem + currentMaxValue;
+                    line.mItemsCount++;
+                } else {
+                    detachAndScrapView(view, recycler);
+                    continue;
+                }
+            } else {
+                if (heightOrWidth > currentMaxValue) {
+                    currentMaxValue = heightOrWidth;
+                    line.mEndValueOfTheHighestItem = line.mStartValueOfTheHighestItem + currentMaxValue;
+                }
+                line.mItemsCount++;
+            }
+
+            currentItemsSize += widthOrHeight + mSpacingBetweenItems;
+
+            currentAdapterIndex++;
+        }
+
+        layoutItemsToEnd(currentItemsSize - mSpacingBetweenItems, currentMaxValue, line.mItemsCount, line.mStartValueOfTheHighestItem);
+
+        return line;
+    }
+
+    /**
+     * Arrange of views from start to end.
+     *
+     * @param itemsSize                  Size(width - if orientation is VERTICAL or height - if orientation is HORIZONTAL) of all items(include spacing) in line.
+     * @param maxItemHeightOrWidth       Max item height(if VERTICAL) or width(if HORIZONTAL) in line.
+     * @param itemsInLine                Item count in line.
+     * @param startValueOfTheHighestItem Start position(Top - if orientation is VERTICAL or Left - if orientation is HORIZONTAL) of the line.
+     */
+    private void layoutItemsToEnd(int itemsSize, int maxItemHeightOrWidth, int itemsInLine, int startValueOfTheHighestItem) {
+        int currentStart = mLayoutManagerHelper.getStartPositionOfFirstItem(itemsSize);
+
+        int i = itemsInLine;
+        int childCount = getChildCount();
+        int currentStartValue;
+
+        while (i > 0) {
+            final View view = getChildAt(childCount - i);
+
+            final int widthOrHeight = mLayoutManagerHelper.getDecoratedMeasurementInOther(view);
+            final int heightOrWidth = mLayoutManagerHelper.getDecoratedMeasurement(view);
+
+            currentStartValue = startValueOfTheHighestItem + mLayoutManagerHelper.getPositionOfCurrentItem(maxItemHeightOrWidth, heightOrWidth);
+
+            if (mOrientation == VERTICAL) {
+                layoutDecoratedWithMargins(view, currentStart, currentStartValue,
+                        currentStart + widthOrHeight, currentStartValue + heightOrWidth);
+            } else {
+                layoutDecoratedWithMargins(view, currentStartValue, currentStart,
+                        currentStartValue + heightOrWidth, currentStart + widthOrHeight);
+            }
+
+            currentStart += widthOrHeight + mSpacingBetweenItems;
+
+            i--;
+        }
+    }
+
+    /**
+     * Add one line to the start of recyclerView.
+     *
+     * @param startAdapterIndex Adapter index of first item of new line.
+     * @param end               End position(Bottom - if orientation is VERTICAL or Right - if orientation is HORIZONTAL) of the new line.
+     * @return New line.
+     */
+    @NonNull
+    private Line addLineToStart(int startAdapterIndex, int end, RecyclerView.Recycler recycler) {
+        boolean isEndOfLine = false;
+
+        int currentAdapterIndex = startAdapterIndex;
+        int currentItemsSize = 0;
+        int currentMaxValue = 0;
+
+        Line line = new Line();
+        line.mEndValueOfTheHighestItem = end;
+
+        while (!isEndOfLine && currentAdapterIndex >= 0) {
+            final View view = recycler.getViewForPosition(currentAdapterIndex);
+
+            addView(view, 0);
+
+            measureChildWithMargins(view, 0, 0);
+
+            final int widthOrHeight = mLayoutManagerHelper.getDecoratedMeasurementInOther(view);
+            final int heightOrWidth = mLayoutManagerHelper.getDecoratedMeasurement(view);
+
+            if (line.mItemsCount == mMaxItemsInLine || (currentItemsSize + widthOrHeight) >= mLayoutManagerHelper.getLineSize()) {
+                isEndOfLine = true;
+
+                if (currentItemsSize == 0) {
+                    currentMaxValue = heightOrWidth;
+
+                    line.mStartValueOfTheHighestItem = line.mEndValueOfTheHighestItem - currentMaxValue;
+                    line.mItemsCount++;
+                } else {
+                    detachAndScrapView(view, recycler);
+                    continue;
+                }
+            } else {
+                if (heightOrWidth > currentMaxValue) {
+                    currentMaxValue = heightOrWidth;
+                    line.mStartValueOfTheHighestItem = line.mEndValueOfTheHighestItem - currentMaxValue;
+                }
+                line.mItemsCount++;
+            }
+
+            currentItemsSize += widthOrHeight + mSpacingBetweenItems;
+
+            currentAdapterIndex--;
+        }
+
+        layoutItemsToStart(currentItemsSize - mSpacingBetweenItems, currentMaxValue, line.mItemsCount, line.mStartValueOfTheHighestItem);
+
+        return line;
+    }
+
+    /**
+     * Arrange of views from end to start.
+     *
+     * @param itemsSize                  Size(width - if orientation is VERTICAL or height - if orientation is HORIZONTAL) of all items(include spacing) in line.
+     * @param maxItemHeightOrWidth       Max item height(if VERTICAL) or width(if HORIZONTAL) in line.
+     * @param itemsInLine                Item count in line.
+     * @param startValueOfTheHighestItem Start position(Top - if orientation is VERTICAL or Left - if orientation is HORIZONTAL) of the line.
+     */
+    private void layoutItemsToStart(int itemsSize, int maxItemHeightOrWidth, int itemsInLine, int startValueOfTheHighestItem) {
+        int currentStart = mLayoutManagerHelper.getStartPositionOfFirstItem(itemsSize);
+
+        int i = 0;
+        int currentStartValue;
+
+        while (i < itemsInLine) {
+            final View view = getChildAt(i);
+
+            final int widthOrHeight = mLayoutManagerHelper.getDecoratedMeasurementInOther(view);
+            final int heightOrWidth = mLayoutManagerHelper.getDecoratedMeasurement(view);
+
+            currentStartValue = startValueOfTheHighestItem + mLayoutManagerHelper.getPositionOfCurrentItem(maxItemHeightOrWidth, heightOrWidth);
+
+            if (mOrientation == VERTICAL) {
+                layoutDecoratedWithMargins(view, currentStart, currentStartValue,
+                        currentStart + widthOrHeight, currentStartValue + heightOrWidth);
+
+            } else {
+                layoutDecoratedWithMargins(view, currentStartValue, currentStart,
+                        currentStartValue + heightOrWidth, currentStart + widthOrHeight);
+            }
+
+            currentStart += widthOrHeight + mSpacingBetweenItems;
+
+            i++;
+        }
+    }
+
+    /**
+     * Adds to start (and delete from end) of the recyclerView the required number of lines depending on the offset.
+     *
+     * @param offset   Original offset.
+     * @param recycler
+     * @return Real offset.
+     */
+    private int addLinesToStartAndDeleteFromEnd(int offset, RecyclerView.Recycler recycler) {
+        Line line = mCurrentLines.get(0);
+
+        final int availableOffset = line.mStartValueOfTheHighestItem - mLayoutManagerHelper.getStartAfterPadding();
+
+        int currentOffset = availableOffset < offset ? offset : availableOffset;
+        int adapterViewIndex = getPosition(getChildAt(0)) - 1;
+
+        int startValueOfNewLine = line.mStartValueOfTheHighestItem - mSpacingBetweenLines;
+
+        while (adapterViewIndex >= 0) {
+
+            if (currentOffset <= offset) {
+                deleteLinesFromEnd(offset, recycler);
+                break;
+            } else {
+                deleteLinesFromEnd(currentOffset, recycler);
+            }
+
+            line = addLineToStart(adapterViewIndex, startValueOfNewLine, recycler);
+            mCurrentLines.add(0, line);
+
+            startValueOfNewLine = line.mStartValueOfTheHighestItem - mSpacingBetweenLines;
+
+            currentOffset = line.mStartValueOfTheHighestItem;
+            adapterViewIndex -= line.mItemsCount;
+        }
+
+        return currentOffset < offset ? offset : currentOffset;
+    }
+
+    /**
+     * Removes lines from the end. The number of deleted lines depends on the offset.
+     *
+     * @param offset   Current offset.
+     * @param recycler
+     */
+    private void deleteLinesFromEnd(int offset, RecyclerView.Recycler recycler) {
+        Line lineToDel = mCurrentLines.get(mCurrentLines.size() - 1);
+
+        while (lineToDel != null) {
+            if (lineToDel.mStartValueOfTheHighestItem - offset >
+                    mLayoutManagerHelper.getEndAfterPadding()) {
+                for (int i = 0; i < lineToDel.mItemsCount; i++) {
+                    removeAndRecycleView(getChildAt(getChildCount() - 1), recycler);
+                }
+                mCurrentLines.remove(lineToDel);
+                lineToDel = mCurrentLines.get(mCurrentLines.size() - 1);
+            } else {
+                lineToDel = null;
+            }
+        }
+    }
+
+    /**
+     * Adds to end (and delete from start) of the recyclerView the required number of lines depending on the offset.
+     *
+     * @param offset   Original offset.
+     * @param recycler
+     * @return Real offset.
+     */
+    private int addLinesToEndAndDeleteFromStart(int offset, RecyclerView.Recycler recycler) {
+        Line line = mCurrentLines.get(mCurrentLines.size() - 1);
+
+        int availableOffset = line.mEndValueOfTheHighestItem - mLayoutManagerHelper.getEnd() + mLayoutManagerHelper.getEndPadding();
+
+        int currentOffset = availableOffset > offset ? offset : availableOffset;
+        int adapterViewIndex = getPosition(getChildAt(getChildCount() - 1)) + 1;
+
+        int startValueOfNewLine = line.mEndValueOfTheHighestItem + mSpacingBetweenLines;
+
+        while (adapterViewIndex < getItemCount()) {
+
+            if (currentOffset >= offset) {
+                deleteLinesFromStart(offset, recycler);
+                break;
+            } else {
+                deleteLinesFromStart(currentOffset, recycler);
+            }
+
+            line = addLineToEnd(adapterViewIndex, startValueOfNewLine, recycler);
+            mCurrentLines.add(line);
+
+            startValueOfNewLine = line.mEndValueOfTheHighestItem + mSpacingBetweenLines;
+
+            currentOffset = line.mEndValueOfTheHighestItem - mLayoutManagerHelper.getEnd();
+            adapterViewIndex += line.mItemsCount;
+        }
+
+
+        return currentOffset > offset ? offset : currentOffset;
+    }
+
+    /**
+     * Removes lines from the start. The number of deleted lines depends on the offset.
+     *
+     * @param offset   Current offset.
+     * @param recycler
+     */
+    private void deleteLinesFromStart(int offset, RecyclerView.Recycler recycler) {
+        Line lineToDel = mCurrentLines.get(0);
+
+        while (lineToDel != null) {
+            if (lineToDel.mEndValueOfTheHighestItem - offset <
+                    mLayoutManagerHelper.getStartAfterPadding()) {
+                for (int i = 0; i < lineToDel.mItemsCount; i++) {
+                    removeAndRecycleView(getChildAt(0), recycler);
+                }
+                mCurrentLines.remove(lineToDel);
+                // mItemsInLines.add(lineToDel.mItemsCount);
+
+                lineToDel = mCurrentLines.get(0);
+            } else {
+                lineToDel = null;
+            }
+        }
+    }
+
+    @Override
+    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
+        int offset = 0;
+
+        if (getChildCount() > 0 && dy != 0) {
+
+            if (dy > 0) {
+                offset = addLinesToEndAndDeleteFromStart(dy, recycler);
+            } else {
+                offset = addLinesToStartAndDeleteFromEnd(dy, recycler);
+            }
+
+            if (offset != 0) {
+                for (int i = 0; i < mCurrentLines.size(); i++) {
+                    mCurrentLines.get(i).offset(-offset);
+                }
+                offsetChildrenVertical(-offset);
+            }
+
+            final View firstView = getChildAt(0);
+
+            mFirstLineStartPosition = mLayoutManagerHelper.getDecoratedStart(firstView);
+            mFirstItemAdapterIndex = getPosition(firstView);
+        }
+
+        return offset;
+    }
+
+    @Override
+    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
+        int offset = 0;
+
+        if (getChildCount() > 0 && dx != 0) {
+
+            if (dx > 0) {
+                offset = addLinesToEndAndDeleteFromStart(dx, recycler);
+            } else {
+                offset = addLinesToStartAndDeleteFromEnd(dx, recycler);
+            }
+
+            if (offset != 0) {
+                for (int i = 0; i < mCurrentLines.size(); i++) {
+                    mCurrentLines.get(i).offset(-offset);
+                }
+                offsetChildrenHorizontal(-offset);
+            }
+
+            final View firstView = getChildAt(0);
+
+            mFirstLineStartPosition = mLayoutManagerHelper.getDecoratedStart(firstView);
+            mFirstItemAdapterIndex = getPosition(firstView);
+        }
+
+        return offset;
+    }
+
+    @Override
+    public boolean canScrollVertically() {
+        return mOrientation == VERTICAL;
+    }
+
+    @Override
+    public boolean canScrollHorizontally() {
+        return mOrientation == HORIZONTAL;
+    }
+
+    @Override
+    public void scrollToPosition(int position) {
+        if (position >= 0 && position <= getItemCount() - 1) {
+            mFirstItemAdapterIndex = position;
+            mFirstLineStartPosition = -1;
+            requestLayout();
+        }
+    }
+
+    @Override
+    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
+        LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext());
+        linearSmoothScroller.setTargetPosition(position);
+
+        startSmoothScroll(linearSmoothScroller);
+    }
+
+    @Override
+    public PointF computeScrollVectorForPosition(int targetPosition) {
+        if (getChildCount() == 0) {
+            return null;
+        }
+
+        final int firstChildPos = getPosition(getChildAt(0));
+        final int direction = targetPosition < firstChildPos ? -1 : 1;
+
+        if (mOrientation == HORIZONTAL) {
+            return new PointF(direction, 0);
+        } else {
+            return new PointF(0, direction);
+        }
+    }
+
+    /**
+     * Representation of line in RecyclerView.
+     */
+    private final static class Line {
+
+        int mStartValueOfTheHighestItem;
+        int mEndValueOfTheHighestItem;
+
+        int mItemsCount;
+
+        void offset(int offset) {
+            mStartValueOfTheHighestItem += offset;
+            mEndValueOfTheHighestItem += offset;
+        }
+    }
+
+    /**
+     * Orientation and gravity helper.
+     */
+    private static abstract class FLMLayoutManagerHelper {
+
+        RecyclerView.LayoutManager mLayoutManager;
+        int mGravity;
+
+        private FLMLayoutManagerHelper(RecyclerView.LayoutManager layoutManager, int gravity) {
+            mLayoutManager = layoutManager;
+            mGravity = gravity;
+        }
+
+        public void setGravity(int gravity) {
+            mGravity = gravity;
+        }
+
+        public abstract int getEnd();
+
+        public abstract int getEndPadding();
+
+        public abstract int getLineSize();
+
+        public abstract int getEndAfterPadding();
+
+        public abstract int getStartAfterPadding();
+
+        public abstract int getDecoratedStart(View view);
+
+        public abstract int getDecoratedMeasurement(View view);
+
+        public abstract int getDecoratedMeasurementInOther(View view);
+
+        public abstract int getStartPositionOfFirstItem(int itemsSize);
+
+        public abstract int getPositionOfCurrentItem(int itemMaxSize, int itemSize);
+
+        public static FLMLayoutManagerHelper createLayoutManagerHelper(RecyclerView.LayoutManager layoutManager, int orientation, int gravity) {
+            switch (orientation) {
+                case VERTICAL:
+                    return createVerticalLayoutManagerHelper(layoutManager, gravity);
+                case HORIZONTAL:
+                    return createHorizontalLayoutManagerHelper(layoutManager, gravity);
+                default:
+                    throw new IllegalArgumentException(ERROR_UNKNOWN_ORIENTATION);
+            }
+        }
+
+        private static FLMLayoutManagerHelper createVerticalLayoutManagerHelper(final RecyclerView.LayoutManager layoutManager, int gravity) {
+            return new FLMLayoutManagerHelper(layoutManager, gravity) {
+                @Override
+                public int getEnd() {
+                    return mLayoutManager.getHeight();
+                }
+
+                @Override
+                public int getEndPadding() {
+                    return mLayoutManager.getPaddingBottom();
+                }
+
+                @Override
+                public int getLineSize() {
+                    return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft() - mLayoutManager.getPaddingRight();
+                }
+
+                @Override
+                public int getEndAfterPadding() {
+                    return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
+                }
+
+                @Override
+                public int getStartAfterPadding() {
+                    return mLayoutManager.getPaddingTop();
+                }
+
+                @Override
+                public int getDecoratedStart(View view) {
+                    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
+
+                    return this.mLayoutManager.getDecoratedTop(view) - params.topMargin;
+                }
+
+                @Override
+                public int getDecoratedMeasurement(View view) {
+                    final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
+
+                    return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
+                }
+
+                @Override
+                public int getDecoratedMeasurementInOther(View view) {
+                    final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
+
+                    return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin;
+                }
+
+                @Override
+                public int getStartPositionOfFirstItem(int itemsSize) {
+                    int horizontalGravity = GravityCompat.getAbsoluteGravity(mGravity, mLayoutManager.getLayoutDirection());
+                    int startPosition;
+
+                    switch (horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+                        case Gravity.RIGHT:
+                            startPosition = mLayoutManager.getWidth() - mLayoutManager.getPaddingRight() - itemsSize;
+                            break;
+                        case Gravity.CENTER_HORIZONTAL:
+                            startPosition = (mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft() - mLayoutManager.getPaddingRight()) / 2
+                                    - itemsSize / 2;
+                            break;
+                        default:
+                            startPosition = mLayoutManager.getPaddingLeft();
+                            break;
+                    }
+
+                    return startPosition;
+                }
+
+                @Override
+                public int getPositionOfCurrentItem(int itemMaxSize, int itemSize) {
+                    int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+                    int currentPosition;
+
+                    switch (verticalGravity) {
+                        case Gravity.CENTER_VERTICAL:
+                            currentPosition = itemMaxSize / 2 - itemSize / 2;
+                            break;
+                        case Gravity.BOTTOM:
+                            currentPosition = itemMaxSize - itemSize;
+                            break;
+                        default:
+                            currentPosition = 0;
+                            break;
+                    }
+                    return currentPosition;
+                }
+
+            };
+        }
+
+        private static FLMLayoutManagerHelper createHorizontalLayoutManagerHelper(RecyclerView.LayoutManager layoutManager, int gravity) {
+            return new FLMLayoutManagerHelper(layoutManager, gravity) {
+
+                @Override
+                public int getEnd() {
+                    return mLayoutManager.getWidth();
+                }
+
+                @Override
+                public int getEndPadding() {
+                    return mLayoutManager.getPaddingRight();
+                }
+
+                @Override
+                public int getLineSize() {
+                    return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop() - mLayoutManager.getPaddingBottom();
+                }
+
+                @Override
+                public int getEndAfterPadding() {
+                    return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
+                }
+
+                @Override
+                public int getStartAfterPadding() {
+                    return mLayoutManager.getPaddingLeft();
+                }
+
+                @Override
+                public int getDecoratedStart(View view) {
+                    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
+
+                    return this.mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
+                }
+
+                @Override
+                public int getDecoratedMeasurement(View view) {
+                    return mLayoutManager.getDecoratedMeasuredWidth(view);
+                }
+
+                @Override
+                public int getDecoratedMeasurementInOther(View view) {
+                    return mLayoutManager.getDecoratedMeasuredHeight(view);
+                }
+
+                @Override
+                public int getStartPositionOfFirstItem(int itemsSize) {
+                    int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+                    int startPosition;
+
+                    switch (verticalGravity) {
+                        case Gravity.CENTER_VERTICAL:
+                            startPosition = (mLayoutManager.getHeight() - mLayoutManager.getPaddingTop() - mLayoutManager.getPaddingBottom()) / 2
+                                    - itemsSize / 2;
+                            break;
+                        case Gravity.BOTTOM:
+                            startPosition = mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom() - itemsSize;
+                            break;
+                        default:
+                            startPosition = mLayoutManager.getPaddingTop();
+                            break;
+                    }
+
+                    return startPosition;
+                }
+
+                @Override
+                public int getPositionOfCurrentItem(int itemMaxSize, int itemSize) {
+                    int horizontalGravity = GravityCompat.getAbsoluteGravity(mGravity, mLayoutManager.getLayoutDirection());
+                    int currentPosition;
+
+                    switch (horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+                        case Gravity.CENTER_HORIZONTAL:
+                            currentPosition = itemMaxSize / 2 - itemSize / 2;
+                            break;
+                        case Gravity.RIGHT:
+                            currentPosition = itemMaxSize - itemSize;
+                            break;
+                        default:
+                            currentPosition = 0;
+                            break;
+                    }
+                    return currentPosition;
+                }
+            };
+        }
+    }
+}

+ 7 - 11
module/playmate/src/main/java/com/adealink/weparty/playmate/detail/adapter/PlaymateDetailCommentLabelViewBinder.kt

@@ -1,20 +1,17 @@
 package com.adealink.weparty.playmate.detail.adapter
 
+import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import com.adealink.frame.base.fastLazy
-import com.adealink.weparty.commonui.drawabletoolbox.DrawableBuilder
 import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
 import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
 import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
+import com.adealink.weparty.commonui.recycleview.layoutmanager.FlowLayoutManager
 import com.adealink.weparty.playmate.databinding.ItemPlaymateDetailCommentLabelBinding
 import com.adealink.weparty.playmate.detail.data.PlaymateCommentLabel
 import com.adealink.weparty.playmate.detail.data.PlaymateDetailCommentLabelItemData
-import com.google.android.flexbox.FlexDirection
-import com.google.android.flexbox.FlexWrap
-import com.google.android.flexbox.FlexboxItemDecoration
-import com.google.android.flexbox.FlexboxLayoutManager
 
 class PlaymateDetailCommentLabelViewBinder() :
     ItemViewBinder<PlaymateDetailCommentLabelItemData, PlaymateDetailCommentLabelViewBinder.ViewHolder>() {
@@ -49,12 +46,11 @@ class PlaymateDetailCommentLabelViewBinder() :
         fun initView() {
             labelAdapter.register(PlaymateCommentLabelViewBinder())
             binding.root.adapter = labelAdapter
-            binding.rvLabel.layoutManager =
-                FlexboxLayoutManager(binding.rvLabel.context, FlexDirection.ROW, FlexWrap.WRAP)
-            binding.rvLabel.addItemDecoration(
-                FlexboxItemDecoration(binding.rvLabel.context).apply {
-                    setDrawable(DrawableBuilder().size(10.dp(), 6.dp()).build())
-                }
+            binding.rvLabel.layoutManager = FlowLayoutManager(
+                FlowLayoutManager.VERTICAL,
+                Gravity.START,
+                10.dp(),
+                6.dp()
             )
         }
 

+ 9 - 13
module/playmate/src/main/java/com/adealink/weparty/playmate/detail/adapter/PlaymateDetailViewBinder.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.playmate.detail.adapter
 
+import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import androidx.constraintlayout.widget.ConstraintLayout
@@ -10,7 +11,6 @@ import com.adealink.frame.base.fastLazy
 import com.adealink.frame.image.view.NetworkImageView
 import com.adealink.frame.util.onClick
 import com.adealink.weparty.commonui.BaseActivity
-import com.adealink.weparty.commonui.drawabletoolbox.DrawableBuilder
 import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.ext.getActivity
 import com.adealink.weparty.commonui.ext.gone
@@ -18,6 +18,7 @@ import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
 import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
 import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
+import com.adealink.weparty.commonui.recycleview.layoutmanager.FlowLayoutManager
 import com.adealink.weparty.commonui.viewpager.RecyclePagerAdapter
 import com.adealink.weparty.playmate.R
 import com.adealink.weparty.playmate.databinding.ItemPlaymateDetailContentBinding
@@ -26,10 +27,6 @@ import com.adealink.weparty.playmate.detail.data.PlaymateDetailImg
 import com.adealink.weparty.playmate.detail.data.PlaymateDetailItemData
 import com.adealink.weparty.playmate.detail.data.PlaymateDetailLabel
 import com.adealink.weparty.util.goImagePreviewActivity
-import com.google.android.flexbox.FlexDirection
-import com.google.android.flexbox.FlexWrap
-import com.google.android.flexbox.FlexboxItemDecoration
-import com.google.android.flexbox.FlexboxLayoutManager
 
 class PlaymateDetailViewBinder(
     val lifecycleOwner: LifecycleOwner
@@ -73,12 +70,11 @@ class PlaymateDetailViewBinder(
         fun initView() {
             labelAdapter.register(PlaymateLabelViewBinder())
             binding.rvLabel.adapter = labelAdapter
-            binding.rvLabel.layoutManager =
-                FlexboxLayoutManager(binding.rvLabel.context, FlexDirection.ROW, FlexWrap.WRAP)
-            binding.rvLabel.addItemDecoration(
-                FlexboxItemDecoration(binding.rvLabel.context).apply {
-                    setDrawable(DrawableBuilder().size(8.dp(), 6.dp()).build())
-                }
+            binding.rvLabel.layoutManager = FlowLayoutManager(
+                FlowLayoutManager.VERTICAL,
+                Gravity.START,
+                8.dp(),
+                6.dp()
             )
 
             binding.bannerImg.pageMargin = 10.dp()
@@ -124,13 +120,13 @@ class PlaymateDetailViewBinder(
                 binding.bannerImg.gone()
             } else {
                 binding.bannerImg.show()
-                if(imageItems.size == 1){
+                if (imageItems.size == 1) {
                     binding.bannerImg.updateLayoutParams<ConstraintLayout.LayoutParams> {
                         dimensionRatio = null
                         marginStart = 16.dp()
                         marginEnd = 16.dp()
                     }
-                }else{
+                } else {
                     binding.bannerImg.updateLayoutParams<ConstraintLayout.LayoutParams> {
                         dimensionRatio = "320:192"
                         marginStart = 16.dp()

+ 7 - 11
module/playmate/src/main/java/com/adealink/weparty/playmate/findpartner/comp/FindPartnerOptionsComp.kt

@@ -1,19 +1,16 @@
 package com.adealink.weparty.playmate.findpartner.comp
 
+import android.view.Gravity
 import androidx.lifecycle.LifecycleOwner
 import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.ViewComponent
-import com.adealink.weparty.commonui.drawabletoolbox.DrawableBuilder
 import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
+import com.adealink.weparty.commonui.recycleview.layoutmanager.FlowLayoutManager
 import com.adealink.weparty.playmate.databinding.LayoutFindPartnerOptionBinding
 import com.adealink.weparty.playmate.findpartner.adapter.OptionItemViewBinder
 import com.adealink.weparty.playmate.findpartner.data.OptionData
 import com.adealink.weparty.playmate.findpartner.data.OptionItemData
-import com.google.android.flexbox.FlexDirection
-import com.google.android.flexbox.FlexWrap
-import com.google.android.flexbox.FlexboxItemDecoration
-import com.google.android.flexbox.FlexboxLayoutManager
 
 class FindPartnerOptionsComp(
     lifecycleOwner: LifecycleOwner,
@@ -39,12 +36,11 @@ class FindPartnerOptionsComp(
         binding.tvTitle.text = title
 
         binding.rvOptions.adapter = optionAdapter
-        binding.rvOptions.layoutManager =
-            FlexboxLayoutManager(binding.rvOptions.context, FlexDirection.ROW, FlexWrap.WRAP)
-        binding.rvOptions.addItemDecoration(
-            FlexboxItemDecoration(binding.rvOptions.context).apply {
-                setDrawable(DrawableBuilder().size(8.dp(), 10.dp()).build())
-            }
+        binding.rvOptions.layoutManager = FlowLayoutManager(
+            FlowLayoutManager.VERTICAL,
+            Gravity.START,
+            8.dp(),
+            10.dp()
         )
         optionAdapter.register(OptionItemViewBinder(object :
             OptionItemViewBinder.OnOptionsSelectListener {

+ 7 - 6
module/profile/src/main/java/com/adealink/weparty/profile/edit/dialog/adapter/InterestListViewBinder.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.profile.edit.dialog.adapter
 
+import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import com.adealink.frame.base.fastLazy
@@ -8,6 +9,7 @@ import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
 import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
 import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
+import com.adealink.weparty.commonui.recycleview.layoutmanager.FlowLayoutManager
 import com.adealink.weparty.profile.databinding.LayoutEditProfileInterestListBinding
 import com.adealink.weparty.ui.category.data.CategoryListData
 import com.adealink.weparty.ui.category.data.CategoryListItemData
@@ -52,12 +54,11 @@ class InterestListViewBinder(
             }))
             rootBinding.rvInterest.adapter = listAdapter
 
-            binding.rvInterest.layoutManager =
-                FlexboxLayoutManager(binding.rvInterest.context, FlexDirection.ROW, FlexWrap.WRAP)
-            binding.rvInterest.addItemDecoration(
-                FlexboxItemDecoration(binding.rvInterest.context).apply {
-                    setDrawable(DrawableBuilder().size(8.dp(), 10.dp()).build())
-                }
+            binding.rvInterest.layoutManager = FlowLayoutManager(
+                FlowLayoutManager.VERTICAL,
+                Gravity.START,
+                8.dp(),
+                10.dp()
             )
         }
 

+ 7 - 11
module/profile/src/main/java/com/adealink/weparty/profile/search/SearchActivity.kt

@@ -2,6 +2,7 @@ package com.adealink.weparty.profile.search
 
 import android.text.Editable
 import android.text.TextWatcher
+import android.view.Gravity
 import android.view.KeyEvent
 import android.view.inputmethod.EditorInfo
 import android.widget.TextView
@@ -23,7 +24,6 @@ import com.adealink.frame.statistics.BaseStatEvent
 import com.adealink.frame.util.onClick
 import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.commonui.BaseActivity
-import com.adealink.weparty.commonui.drawabletoolbox.DrawableBuilder
 import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.ext.fitSystemWindows
 import com.adealink.weparty.commonui.ext.gone
@@ -32,6 +32,7 @@ import com.adealink.weparty.commonui.ext.onWindowInsets
 import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
 import com.adealink.weparty.commonui.recycleview.itemdecoration.VerticalSpaceItemDecoration
+import com.adealink.weparty.commonui.recycleview.layoutmanager.FlowLayoutManager
 import com.adealink.weparty.commonui.toast.util.showFailedToast
 import com.adealink.weparty.module.playmate.event.PlaymateListEventReporter
 import com.adealink.weparty.module.playmate.event.PlaymateStatEvent
@@ -45,10 +46,6 @@ import com.adealink.weparty.profile.search.adapter.SearchItemViewBinder
 import com.adealink.weparty.profile.search.data.SearchHistoryItemData
 import com.adealink.weparty.profile.search.data.SearchResultItemData
 import com.adealink.weparty.profile.search.viewmodel.SearchViewModel
-import com.google.android.flexbox.FlexDirection
-import com.google.android.flexbox.FlexWrap
-import com.google.android.flexbox.FlexboxItemDecoration
-import com.google.android.flexbox.FlexboxLayoutManager
 import com.qmuiteam.qmui.widget.util.QMUIKeyboardHelper
 import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
 import com.adealink.weparty.R as APP_R
@@ -119,12 +116,11 @@ class SearchActivity : BaseActivity(),
         }
 
         historyAdapter.register(SearchHistoryViewBinder(this))
-        binding.rvHistory.layoutManager =
-            FlexboxLayoutManager(binding.rvHistory.context, FlexDirection.ROW, FlexWrap.WRAP)
-        binding.rvHistory.addItemDecoration(
-            FlexboxItemDecoration(binding.rvHistory.context).apply {
-                setDrawable(DrawableBuilder().size(8.dp(), 10.dp()).build())
-            }
+        binding.rvHistory.layoutManager = FlowLayoutManager(
+            FlowLayoutManager.VERTICAL,
+            Gravity.START,
+            8.dp(),
+            10.dp()
         )
         binding.rvHistory.adapter = historyAdapter
 

+ 7 - 11
module/profile/src/main/java/com/adealink/weparty/profile/ui/about/ProfileFragment.kt

@@ -1,6 +1,7 @@
 package com.adealink.weparty.profile.ui.about
 
 import android.text.SpannableStringBuilder
+import android.view.Gravity
 import androidx.annotation.DrawableRes
 import androidx.fragment.app.activityViewModels
 import androidx.recyclerview.widget.LinearLayoutManager
@@ -11,12 +12,12 @@ import com.adealink.frame.ext.findAndSetSpan
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.weparty.commonui.BaseFragment
-import com.adealink.weparty.commonui.drawabletoolbox.DrawableBuilder
 import com.adealink.weparty.commonui.ext.dp
 import com.adealink.weparty.commonui.ext.gone
 import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.MultiTypeListAdapter
 import com.adealink.weparty.commonui.recycleview.itemdecoration.VerticalSpaceItemDecoration
+import com.adealink.weparty.commonui.recycleview.layoutmanager.FlowLayoutManager
 import com.adealink.weparty.commonui.widget.CenterImageSpan
 import com.adealink.weparty.module.playmate.Playmate
 import com.adealink.weparty.module.profile.ProfileModule
@@ -25,10 +26,6 @@ import com.adealink.weparty.profile.R
 import com.adealink.weparty.profile.databinding.FragmentProfileBinding
 import com.adealink.weparty.profile.viewmodel.ProfileViewModel
 import com.adealink.weparty.profile.viewmodel.ProfileViewModelFactory
-import com.google.android.flexbox.FlexDirection
-import com.google.android.flexbox.FlexWrap
-import com.google.android.flexbox.FlexboxItemDecoration
-import com.google.android.flexbox.FlexboxLayoutManager
 import com.adealink.weparty.R as APP_R
 
 class ProfileFragment : BaseFragment(R.layout.fragment_profile) {
@@ -45,12 +42,11 @@ class ProfileFragment : BaseFragment(R.layout.fragment_profile) {
 
         interestAdapter.register(ProfileInterestViewBinder())
         binding.rvInterest.adapter = interestAdapter
-        binding.rvInterest.layoutManager =
-            FlexboxLayoutManager(binding.rvInterest.context, FlexDirection.ROW, FlexWrap.WRAP)
-        binding.rvInterest.addItemDecoration(
-            FlexboxItemDecoration(binding.rvInterest.context).apply {
-                setDrawable(DrawableBuilder().size(10.dp(), 6.dp()).build())
-            }
+        binding.rvInterest.layoutManager = FlowLayoutManager(
+            FlowLayoutManager.VERTICAL,
+            Gravity.START,
+            10.dp(),
+            6.dp()
         )
 
         skillAdapter.register(