|
|
@@ -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;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|