diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/AutoPlayRecyclerView.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/AutoPlayRecyclerView.java new file mode 100644 index 0000000..5afd7cb --- /dev/null +++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/AutoPlayRecyclerView.java @@ -0,0 +1,73 @@ +package com.heyongrui.base.widget.layoutmanager; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * An implement of {@link RecyclerView} which support auto play. + */ + +public class AutoPlayRecyclerView extends RecyclerView { + + private AutoPlaySnapHelper autoPlaySnapHelper; + + public AutoPlayRecyclerView(Context context) { + this(context, null); + } + + public AutoPlayRecyclerView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public AutoPlayRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + autoPlaySnapHelper = new AutoPlaySnapHelper(AutoPlaySnapHelper.TIME_INTERVAL, AutoPlaySnapHelper.RIGHT); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + boolean result = super.dispatchTouchEvent(ev); + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + if (autoPlaySnapHelper != null) { + autoPlaySnapHelper.pause(); + } + break; + case MotionEvent.ACTION_UP: + if (autoPlaySnapHelper != null) { + autoPlaySnapHelper.start(); + } + } + return result; + } + + public void setTimeInterval(int timeInterval) { + if (null != autoPlaySnapHelper) { + autoPlaySnapHelper.setTimeInterval(timeInterval); + } + } + + public void setDirection(int direction) { + if (null != autoPlaySnapHelper) { + autoPlaySnapHelper.setDirection(direction); + } + } + + public void start() { + autoPlaySnapHelper.start(); + } + + public void pause() { + autoPlaySnapHelper.pause(); + } + + @Override + public void setLayoutManager(LayoutManager layout) { + super.setLayoutManager(layout); + autoPlaySnapHelper.attachToRecyclerView(this); + } +} diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/AutoPlaySnapHelper.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/AutoPlaySnapHelper.java new file mode 100644 index 0000000..b9d48e7 --- /dev/null +++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/AutoPlaySnapHelper.java @@ -0,0 +1,116 @@ +package com.heyongrui.base.widget.layoutmanager; + +import android.os.Handler; +import android.os.Looper; +import android.view.animation.DecelerateInterpolator; +import android.widget.Scroller; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + + +/** + * Used by {@link AutoPlayRecyclerView} to implement auto play effect + */ + +class AutoPlaySnapHelper extends CenterSnapHelper { + final static int TIME_INTERVAL = 2000; + + final static int LEFT = 1; + final static int RIGHT = 2; + + private Handler handler; + private int timeInterval; + private Runnable autoPlayRunnable; + private boolean runnableAdded; + private int direction; + + AutoPlaySnapHelper(int timeInterval, int direction) { + checkTimeInterval(timeInterval); + checkDirection(direction); + handler = new Handler(Looper.getMainLooper()); + this.timeInterval = timeInterval; + this.direction = direction; + } + + @Override + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException { + if (mRecyclerView == recyclerView) { + return; // nothing to do + } + if (mRecyclerView != null) { + destroyCallbacks(); + } + mRecyclerView = recyclerView; + if (mRecyclerView != null) { + final RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); + if (!(layoutManager instanceof ViewPagerLayoutManager)) return; + + setupCallbacks(); + mGravityScroller = new Scroller(mRecyclerView.getContext(), + new DecelerateInterpolator()); + + snapToCenterView((ViewPagerLayoutManager) layoutManager, + ((ViewPagerLayoutManager) layoutManager).onPageChangeListener); + + ((ViewPagerLayoutManager) layoutManager).setInfinite(true); + + autoPlayRunnable = new Runnable() { + @Override + public void run() { + final int currentPosition = + ((ViewPagerLayoutManager) layoutManager).getCurrentPositionOffset() * + (((ViewPagerLayoutManager) layoutManager).getReverseLayout() ? -1 : 1); + ScrollHelper.smoothScrollToPosition(mRecyclerView, + (ViewPagerLayoutManager) layoutManager, direction == RIGHT ? currentPosition + 1 : currentPosition - 1); + handler.postDelayed(autoPlayRunnable, timeInterval); + } + }; + handler.postDelayed(autoPlayRunnable, timeInterval); + runnableAdded = true; + } + } + + @Override + void destroyCallbacks() { + super.destroyCallbacks(); + if (runnableAdded) { + handler.removeCallbacks(autoPlayRunnable); + runnableAdded = false; + } + } + + void pause() { + if (runnableAdded) { + handler.removeCallbacks(autoPlayRunnable); + runnableAdded = false; + } + } + + void start() { + if (!runnableAdded) { + handler.postDelayed(autoPlayRunnable, timeInterval); + runnableAdded = true; + } + } + + void setTimeInterval(int timeInterval) { + checkTimeInterval(timeInterval); + this.timeInterval = timeInterval; + } + + void setDirection(int direction) { + checkDirection(direction); + this.direction = direction; + } + + private void checkDirection(int direction) { + if (direction != LEFT && direction != RIGHT) + throw new IllegalArgumentException("direction should be one of left or right"); + } + + private void checkTimeInterval(int timeInterval) { + if (timeInterval <= 0) + throw new IllegalArgumentException("time interval should greater than 0"); + } +} diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CarouselLayoutManager.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CarouselLayoutManager.java new file mode 100644 index 0000000..744f707 --- /dev/null +++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CarouselLayoutManager.java @@ -0,0 +1,166 @@ +package com.heyongrui.base.widget.layoutmanager; + +import android.content.Context; +import android.view.View; + +/** + * An implementation of {@link ViewPagerLayoutManager} + * which layouts items like carousel + */ + +public class CarouselLayoutManager extends ViewPagerLayoutManager { + + private int itemSpace; + private float minScale; + private float moveSpeed; + + public CarouselLayoutManager(Context context, int itemSpace) { + this(new Builder(context, itemSpace)); + } + + public CarouselLayoutManager(Context context, int itemSpace, int orientation) { + this(new Builder(context, itemSpace).setOrientation(orientation)); + } + + public CarouselLayoutManager(Context context, int itemSpace, int orientation, boolean reverseLayout) { + this(new Builder(context, itemSpace).setOrientation(orientation).setReverseLayout(reverseLayout)); + } + + public CarouselLayoutManager(Builder builder) { + this(builder.context, builder.itemSpace, builder.minScale, builder.orientation, + builder.maxVisibleItemCount, builder.moveSpeed, builder.distanceToBottom, + builder.reverseLayout); + } + + private CarouselLayoutManager(Context context, int itemSpace, float minScale, int orientation, + int maxVisibleItemCount, float moveSpeed, int distanceToBottom, + boolean reverseLayout) { + super(context, orientation, reverseLayout); + setEnableBringCenterToFront(true); + setDistanceToBottom(distanceToBottom); + setMaxVisibleItemCount(maxVisibleItemCount); + this.itemSpace = itemSpace; + this.minScale = minScale; + this.moveSpeed = moveSpeed; + } + + public int getItemSpace() { + return itemSpace; + } + + public float getMinScale() { + return minScale; + } + + public float getMoveSpeed() { + return moveSpeed; + } + + public void setItemSpace(int itemSpace) { + assertNotInLayoutOrScroll(null); + if (this.itemSpace == itemSpace) return; + this.itemSpace = itemSpace; + removeAllViews(); + } + + public void setMinScale(float minScale) { + assertNotInLayoutOrScroll(null); + if (minScale > 1f) minScale = 1f; + if (this.minScale == minScale) return; + this.minScale = minScale; + requestLayout(); + } + + public void setMoveSpeed(float moveSpeed) { + assertNotInLayoutOrScroll(null); + if (this.moveSpeed == moveSpeed) return; + this.moveSpeed = moveSpeed; + } + + @Override + protected float setInterval() { + return (mDecoratedMeasurement - itemSpace); + } + + @Override + protected void setItemViewProperty(View itemView, float targetOffset) { + float scale = calculateScale(targetOffset + mSpaceMain); + itemView.setScaleX(scale); + itemView.setScaleY(scale); + } + + @Override + protected float getDistanceRatio() { + if (moveSpeed == 0) return Float.MAX_VALUE; + return 1 / moveSpeed; + } + + @Override + protected float setViewElevation(View itemView, float targetOffset) { + return itemView.getScaleX() * 5; + } + + private float calculateScale(float x) { + float deltaX = Math.abs(x - (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2f); + return (minScale - 1) * deltaX / (mOrientationHelper.getTotalSpace() / 2f) + 1f; + } + + public static class Builder { + private static final float DEFAULT_SPEED = 1f; + private static final float MIN_SCALE = 0.5f; + + private Context context; + private int itemSpace; + private int orientation; + private float minScale; + private float moveSpeed; + private int maxVisibleItemCount; + private boolean reverseLayout; + private int distanceToBottom; + + public Builder(Context context, int itemSpace) { + this.itemSpace = itemSpace; + this.context = context; + orientation = HORIZONTAL; + minScale = MIN_SCALE; + this.moveSpeed = DEFAULT_SPEED; + reverseLayout = false; + maxVisibleItemCount = ViewPagerLayoutManager.DETERMINE_BY_MAX_AND_MIN; + distanceToBottom = ViewPagerLayoutManager.INVALID_SIZE; + } + + public Builder setOrientation(int orientation) { + this.orientation = orientation; + return this; + } + + public Builder setMinScale(float minScale) { + this.minScale = minScale; + return this; + } + + public Builder setReverseLayout(boolean reverseLayout) { + this.reverseLayout = reverseLayout; + return this; + } + + public Builder setMoveSpeed(float moveSpeed) { + this.moveSpeed = moveSpeed; + return this; + } + + public Builder setMaxVisibleItemCount(int maxVisibleItemCount) { + this.maxVisibleItemCount = maxVisibleItemCount; + return this; + } + + public Builder setDistanceToBottom(int distanceToBottom) { + this.distanceToBottom = distanceToBottom; + return this; + } + + public CarouselLayoutManager build() { + return new CarouselLayoutManager(this); + } + } +} diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CenterSnapHelper.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CenterSnapHelper.java new file mode 100644 index 0000000..db8c161 --- /dev/null +++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CenterSnapHelper.java @@ -0,0 +1,176 @@ +package com.heyongrui.base.widget.layoutmanager; + +import android.view.animation.DecelerateInterpolator; +import android.widget.Scroller; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Class intended to support snapping for a {@link RecyclerView} + * which use {@link ViewPagerLayoutManager} as its {@link RecyclerView.LayoutManager}. + *
+ * The implementation will snap the center of the target child view to the center of + * the attached {@link RecyclerView}. + */ +public class CenterSnapHelper extends RecyclerView.OnFlingListener { + + RecyclerView mRecyclerView; + Scroller mGravityScroller; + + /** + * when the dataSet is extremely large + * {@link #snapToCenterView(ViewPagerLayoutManager, ViewPagerLayoutManager.OnPageChangeListener)} + * may keep calling itself because the accuracy of float + */ + private boolean snapToCenter = false; + + // Handles the snap on scroll case. + private final RecyclerView.OnScrollListener mScrollListener = + new RecyclerView.OnScrollListener() { + + boolean mScrolled = false; + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + + final ViewPagerLayoutManager layoutManager = + (ViewPagerLayoutManager) recyclerView.getLayoutManager(); + final ViewPagerLayoutManager.OnPageChangeListener onPageChangeListener = + layoutManager.onPageChangeListener; + if (onPageChangeListener != null) { + onPageChangeListener.onPageScrollStateChanged(newState); + } + + if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) { + mScrolled = false; + if (!snapToCenter) { + snapToCenter = true; + snapToCenterView(layoutManager, onPageChangeListener); + } else { + snapToCenter = false; + } + } + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (dx != 0 || dy != 0) { + mScrolled = true; + } + } + }; + + @Override + public boolean onFling(int velocityX, int velocityY) { + ViewPagerLayoutManager layoutManager = (ViewPagerLayoutManager) mRecyclerView.getLayoutManager(); + if (layoutManager == null) { + return false; + } + RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); + if (adapter == null) { + return false; + } + + if (!layoutManager.getInfinite() && + (layoutManager.mOffset == layoutManager.getMaxOffset() + || layoutManager.mOffset == layoutManager.getMinOffset())) { + return false; + } + + final int minFlingVelocity = mRecyclerView.getMinFlingVelocity(); + mGravityScroller.fling(0, 0, velocityX, velocityY, + Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); + + if (layoutManager.mOrientation == ViewPagerLayoutManager.VERTICAL + && Math.abs(velocityY) > minFlingVelocity) { + final int currentPosition = layoutManager.getCurrentPositionOffset(); + final int offsetPosition = (int) (mGravityScroller.getFinalY() / + layoutManager.mInterval / layoutManager.getDistanceRatio()); + ScrollHelper.smoothScrollToPosition(mRecyclerView, layoutManager, layoutManager.getReverseLayout() ? + -currentPosition - offsetPosition : currentPosition + offsetPosition); + return true; + } else if (layoutManager.mOrientation == ViewPagerLayoutManager.HORIZONTAL + && Math.abs(velocityX) > minFlingVelocity) { + final int currentPosition = layoutManager.getCurrentPositionOffset(); + final int offsetPosition = (int) (mGravityScroller.getFinalX() / + layoutManager.mInterval / layoutManager.getDistanceRatio()); + ScrollHelper.smoothScrollToPosition(mRecyclerView, layoutManager, layoutManager.getReverseLayout() ? + -currentPosition - offsetPosition : currentPosition + offsetPosition); + return true; + } + + return true; + } + + /** + * Please attach after {{@link RecyclerView.LayoutManager} is setting} + * Attaches the {@link CenterSnapHelper} to the provided RecyclerView, by calling + * {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}. + * You can call this method with {@code null} to detach it from the current RecyclerView. + * + * @param recyclerView The RecyclerView instance to which you want to add this helper or + * {@code null} if you want to remove CenterSnapHelper from the current + * RecyclerView. + * @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener} + * attached to the provided {@link RecyclerView}. + */ + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) + throws IllegalStateException { + if (mRecyclerView == recyclerView) { + return; // nothing to do + } + if (mRecyclerView != null) { + destroyCallbacks(); + } + mRecyclerView = recyclerView; + if (mRecyclerView != null) { + final RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); + if (!(layoutManager instanceof ViewPagerLayoutManager)) return; + + setupCallbacks(); + mGravityScroller = new Scroller(mRecyclerView.getContext(), + new DecelerateInterpolator()); + + snapToCenterView((ViewPagerLayoutManager) layoutManager, + ((ViewPagerLayoutManager) layoutManager).onPageChangeListener); + } + } + + void snapToCenterView(ViewPagerLayoutManager layoutManager, + ViewPagerLayoutManager.OnPageChangeListener listener) { + final int delta = layoutManager.getOffsetToCenter(); + if (delta != 0) { + if (layoutManager.getOrientation() == ViewPagerLayoutManager.VERTICAL) + mRecyclerView.smoothScrollBy(0, delta); + else + mRecyclerView.smoothScrollBy(delta, 0); + } else { + // set it false to make smoothScrollToPosition keep trigger the listener + snapToCenter = false; + } + + if (listener != null) + listener.onPageSelected(layoutManager.getCurrentPosition()); + } + + /** + * Called when an instance of a {@link RecyclerView} is attached. + */ + void setupCallbacks() throws IllegalStateException { + if (mRecyclerView.getOnFlingListener() != null) { + throw new IllegalStateException("An instance of OnFlingListener already set."); + } + mRecyclerView.addOnScrollListener(mScrollListener); + mRecyclerView.setOnFlingListener(this); + } + + /** + * Called when the instance of a {@link RecyclerView} is detached. + */ + void destroyCallbacks() { + mRecyclerView.removeOnScrollListener(mScrollListener); + mRecyclerView.setOnFlingListener(null); + } +} diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CircleLayoutManager.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CircleLayoutManager.java new file mode 100644 index 0000000..9976af5 --- /dev/null +++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CircleLayoutManager.java @@ -0,0 +1,355 @@ +package com.heyongrui.base.widget.layoutmanager; + +import android.content.Context; +import android.view.View; + +/** + * An implementation of {@link ViewPagerLayoutManager} + * which layouts item in a circle + */ + +@SuppressWarnings({"WeakerAccess", "unused"}) +public class CircleLayoutManager extends ViewPagerLayoutManager { + public static final int LEFT = 10; + public static final int RIGHT = 11; + public static final int TOP = 12; + public static final int BOTTOM = 13; + + public static final int LEFT_ON_TOP = 4; + public static final int RIGHT_ON_TOP = 5; + public static final int CENTER_ON_TOP = 6; + + private int radius; + private int angleInterval; + private float moveSpeed; + private float maxRemoveAngle; + private float minRemoveAngle; + private int gravity; + private boolean flipRotate; + private int zAlignment; + + public CircleLayoutManager(Context context) { + this(new Builder(context)); + } + + public CircleLayoutManager(Context context, boolean reverseLayout) { + this(new Builder(context).setReverseLayout(reverseLayout)); + } + + public CircleLayoutManager(Context context, int gravity, boolean reverseLayout) { + this(new Builder(context).setGravity(gravity).setReverseLayout(reverseLayout)); + } + + public CircleLayoutManager(Builder builder) { + this(builder.context, builder.radius, builder.angleInterval, builder.moveSpeed, builder.maxRemoveAngle, + builder.minRemoveAngle, builder.gravity, builder.zAlignment, builder.flipRotate, + builder.maxVisibleItemCount, builder.distanceToBottom, builder.reverseLayout); + } + + private CircleLayoutManager(Context context, int radius, int angleInterval, float moveSpeed, + float max, float min, int gravity, int zAlignment, boolean flipRotate, + int maxVisibleItemCount, int distanceToBottom, boolean reverseLayout) { + super(context, (gravity == LEFT || gravity == RIGHT) ? VERTICAL : HORIZONTAL, reverseLayout); + setEnableBringCenterToFront(true); + setMaxVisibleItemCount(maxVisibleItemCount); + setDistanceToBottom(distanceToBottom); + this.radius = radius; + this.angleInterval = angleInterval; + this.moveSpeed = moveSpeed; + this.maxRemoveAngle = max; + this.minRemoveAngle = min; + this.gravity = gravity; + this.flipRotate = flipRotate; + this.zAlignment = zAlignment; + } + + public int getRadius() { + return radius; + } + + public int getAngleInterval() { + return angleInterval; + } + + public float getMoveSpeed() { + return moveSpeed; + } + + public float getMaxRemoveAngle() { + return maxRemoveAngle; + } + + public float getMinRemoveAngle() { + return minRemoveAngle; + } + + public int getGravity() { + return gravity; + } + + public boolean getFlipRotate() { + return flipRotate; + } + + public int getZAlignment() { + return zAlignment; + } + + public void setRadius(int radius) { + assertNotInLayoutOrScroll(null); + if (this.radius == radius) return; + this.radius = radius; + removeAllViews(); + } + + public void setAngleInterval(int angleInterval) { + assertNotInLayoutOrScroll(null); + if (this.angleInterval == angleInterval) return; + this.angleInterval = angleInterval; + removeAllViews(); + } + + public void setMoveSpeed(float moveSpeed) { + assertNotInLayoutOrScroll(null); + if (this.moveSpeed == moveSpeed) return; + this.moveSpeed = moveSpeed; + } + + public void setMaxRemoveAngle(float maxRemoveAngle) { + assertNotInLayoutOrScroll(null); + if (this.maxRemoveAngle == maxRemoveAngle) return; + this.maxRemoveAngle = maxRemoveAngle; + requestLayout(); + } + + public void setMinRemoveAngle(float minRemoveAngle) { + assertNotInLayoutOrScroll(null); + if (this.minRemoveAngle == minRemoveAngle) return; + this.minRemoveAngle = minRemoveAngle; + requestLayout(); + } + + public void setGravity(int gravity) { + assertNotInLayoutOrScroll(null); + assertGravity(gravity); + if (this.gravity == gravity) return; + this.gravity = gravity; + if (gravity == LEFT || gravity == RIGHT) { + setOrientation(VERTICAL); + } else { + setOrientation(HORIZONTAL); + } + requestLayout(); + } + + public void setFlipRotate(boolean flipRotate) { + assertNotInLayoutOrScroll(null); + if (this.flipRotate == flipRotate) return; + this.flipRotate = flipRotate; + requestLayout(); + } + + public void setZAlignment(int zAlignment) { + assertNotInLayoutOrScroll(null); + assertZAlignmentState(zAlignment); + if (this.zAlignment == zAlignment) return; + this.zAlignment = zAlignment; + requestLayout(); + } + + @Override + protected float setInterval() { + return angleInterval; + } + + @Override + protected void setUp() { + radius = radius == Builder.INVALID_VALUE ? mDecoratedMeasurementInOther : radius; + } + + @Override + protected float maxRemoveOffset() { + return maxRemoveAngle; + } + + @Override + protected float minRemoveOffset() { + return minRemoveAngle; + } + + @Override + protected int calItemLeft(View itemView, float targetOffset) { + switch (gravity) { + case LEFT: + return (int) (radius * Math.sin(Math.toRadians(90 - targetOffset)) - radius); + case RIGHT: + return (int) (radius - radius * Math.sin(Math.toRadians(90 - targetOffset))); + case TOP: + case BOTTOM: + default: + return (int) (radius * Math.cos(Math.toRadians(90 - targetOffset))); + } + } + + @Override + protected int calItemTop(View itemView, float targetOffset) { + switch (gravity) { + case LEFT: + case RIGHT: + return (int) (radius * Math.cos(Math.toRadians(90 - targetOffset))); + case TOP: + return (int) (radius * Math.sin(Math.toRadians(90 - targetOffset)) - radius); + case BOTTOM: + default: + return (int) (radius - radius * Math.sin(Math.toRadians(90 - targetOffset))); + } + } + + @Override + protected void setItemViewProperty(View itemView, float targetOffset) { + switch (gravity) { + case RIGHT: + case TOP: + if (flipRotate) { + itemView.setRotation(targetOffset); + } else { + itemView.setRotation(360 - targetOffset); + } + break; + case LEFT: + case BOTTOM: + default: + if (flipRotate) { + itemView.setRotation(360 - targetOffset); + } else { + itemView.setRotation(targetOffset); + } + break; + } + } + + @Override + protected float setViewElevation(View itemView, float targetOffset) { + if (zAlignment == LEFT_ON_TOP) + return (540 - targetOffset) / 72; + else if (zAlignment == RIGHT_ON_TOP) + return (targetOffset - 540) / 72; + else + return (360 - Math.abs(targetOffset)) / 72; + } + + @Override + protected float getDistanceRatio() { + if (moveSpeed == 0) return Float.MAX_VALUE; + return 1 / moveSpeed; + } + + private static void assertGravity(int gravity) { + if (gravity != LEFT && gravity != RIGHT && gravity != TOP && gravity != BOTTOM) { + throw new IllegalArgumentException("gravity must be one of LEFT RIGHT TOP and BOTTOM"); + } + } + + private static void assertZAlignmentState(int zAlignment) { + if (zAlignment != LEFT_ON_TOP && zAlignment != RIGHT_ON_TOP && zAlignment != CENTER_ON_TOP) { + throw new IllegalArgumentException("zAlignment must be one of LEFT_ON_TOP RIGHT_ON_TOP and CENTER_ON_TOP"); + } + } + + public static class Builder { + private static int INTERVAL_ANGLE = 30;// The default mInterval angle between each items + private static float DISTANCE_RATIO = 10f; // Finger swipe distance divide item rotate angle + private static int INVALID_VALUE = Integer.MIN_VALUE; + private static int MAX_REMOVE_ANGLE = 90; + private static int MIN_REMOVE_ANGLE = -90; + + private int radius; + private int angleInterval; + private float moveSpeed; + private float maxRemoveAngle; + private float minRemoveAngle; + private boolean reverseLayout; + private Context context; + private int gravity; + private boolean flipRotate; + private int zAlignment; + private int maxVisibleItemCount; + private int distanceToBottom; + + public Builder(Context context) { + this.context = context; + radius = INVALID_VALUE; + angleInterval = INTERVAL_ANGLE; + moveSpeed = 1 / DISTANCE_RATIO; + maxRemoveAngle = MAX_REMOVE_ANGLE; + minRemoveAngle = MIN_REMOVE_ANGLE; + reverseLayout = false; + flipRotate = false; + gravity = BOTTOM; + zAlignment = LEFT_ON_TOP; + maxVisibleItemCount = ViewPagerLayoutManager.DETERMINE_BY_MAX_AND_MIN; + distanceToBottom = ViewPagerLayoutManager.INVALID_SIZE; + } + + public Builder setRadius(int radius) { + this.radius = radius; + return this; + } + + public Builder setAngleInterval(int angleInterval) { + this.angleInterval = angleInterval; + return this; + } + + public Builder setMoveSpeed(int moveSpeed) { + this.moveSpeed = moveSpeed; + return this; + } + + public Builder setMaxRemoveAngle(float maxRemoveAngle) { + this.maxRemoveAngle = maxRemoveAngle; + return this; + } + + public Builder setMinRemoveAngle(float minRemoveAngle) { + this.minRemoveAngle = minRemoveAngle; + return this; + } + + public Builder setReverseLayout(boolean reverseLayout) { + this.reverseLayout = reverseLayout; + return this; + } + + public Builder setGravity(int gravity) { + assertGravity(gravity); + this.gravity = gravity; + return this; + } + + public Builder setFlipRotate(boolean flipRotate) { + this.flipRotate = flipRotate; + return this; + } + + public Builder setZAlignment(int zAlignment) { + assertZAlignmentState(zAlignment); + this.zAlignment = zAlignment; + return this; + } + + public Builder setMaxVisibleItemCount(int maxVisibleItemCount) { + this.maxVisibleItemCount = maxVisibleItemCount; + return this; + } + + public Builder setDistanceToBottom(int distanceToBottom) { + this.distanceToBottom = distanceToBottom; + return this; + } + + public CircleLayoutManager build() { + return new CircleLayoutManager(this); + } + } +} diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CircleScaleLayoutManager.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CircleScaleLayoutManager.java new file mode 100644 index 0000000..7f16ea5 --- /dev/null +++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/CircleScaleLayoutManager.java @@ -0,0 +1,393 @@ +package com.heyongrui.base.widget.layoutmanager; + +import android.content.Context; +import android.view.View; + +/** + * An implementation of {@link ViewPagerLayoutManager} + * which layouts item in a circle and will change the child's centerScale while scrolling + */ + +@SuppressWarnings({"WeakerAccess", "unused"}) +public class CircleScaleLayoutManager extends ViewPagerLayoutManager { + public static final int LEFT = 10; + public static final int RIGHT = 11; + public static final int TOP = 12; + public static final int BOTTOM = 13; + + public static final int LEFT_ON_TOP = 4; + public static final int RIGHT_ON_TOP = 5; + public static final int CENTER_ON_TOP = 6; + + private int radius; + private int angleInterval; + private float moveSpeed; + private float centerScale; + private float maxRemoveAngle; + private float minRemoveAngle; + private int gravity; + private boolean flipRotate; + private int zAlignment; + + public CircleScaleLayoutManager(Context context) { + this(new Builder(context)); + } + + public CircleScaleLayoutManager(Context context, int gravity, boolean reverseLayout) { + this(new Builder(context).setGravity(gravity).setReverseLayout(reverseLayout)); + } + + public CircleScaleLayoutManager(Context context, boolean reverseLayout) { + this(new Builder(context).setReverseLayout(reverseLayout)); + } + + public CircleScaleLayoutManager(Builder builder) { + this(builder.context, builder.radius, builder.angleInterval, builder.centerScale, builder.moveSpeed, + builder.maxRemoveAngle, builder.minRemoveAngle, builder.gravity, builder.zAlignment, + builder.flipRotate, builder.maxVisibleItemCount, builder.distanceToBottom, builder.reverseLayout); + } + + private CircleScaleLayoutManager(Context context, int radius, int angleInterval, float centerScale, + float moveSpeed, float max, float min, int gravity, int zAlignment, + boolean flipRotate, int maxVisibleItemCount, int distanceToBottom, boolean reverseLayout) { + super(context, HORIZONTAL, reverseLayout); + setEnableBringCenterToFront(true); + setMaxVisibleItemCount(maxVisibleItemCount); + setDistanceToBottom(distanceToBottom); + this.radius = radius; + this.angleInterval = angleInterval; + this.centerScale = centerScale; + this.moveSpeed = moveSpeed; + this.maxRemoveAngle = max; + this.minRemoveAngle = min; + this.gravity = gravity; + this.flipRotate = flipRotate; + this.zAlignment = zAlignment; + } + + public int getRadius() { + return radius; + } + + public int getAngleInterval() { + return angleInterval; + } + + public float getCenterScale() { + return centerScale; + } + + public float getMoveSpeed() { + return moveSpeed; + } + + public float getMaxRemoveAngle() { + return maxRemoveAngle; + } + + public float getMinRemoveAngle() { + return minRemoveAngle; + } + + public int getGravity() { + return gravity; + } + + public boolean getFlipRotate() { + return flipRotate; + } + + public int getZAlignment() { + return zAlignment; + } + + public void setRadius(int radius) { + assertNotInLayoutOrScroll(null); + if (this.radius == radius) return; + this.radius = radius; + removeAllViews(); + } + + public void setAngleInterval(int angleInterval) { + assertNotInLayoutOrScroll(null); + if (this.angleInterval == angleInterval) return; + this.angleInterval = angleInterval; + removeAllViews(); + } + + public void setCenterScale(float centerScale) { + assertNotInLayoutOrScroll(null); + if (this.centerScale == centerScale) return; + this.centerScale = centerScale; + requestLayout(); + } + + public void setMoveSpeed(float moveSpeed) { + assertNotInLayoutOrScroll(null); + if (this.moveSpeed == moveSpeed) return; + this.moveSpeed = moveSpeed; + } + + public void setMaxRemoveAngle(float maxRemoveAngle) { + assertNotInLayoutOrScroll(null); + if (this.maxRemoveAngle == maxRemoveAngle) return; + this.maxRemoveAngle = maxRemoveAngle; + requestLayout(); + } + + public void setMinRemoveAngle(float minRemoveAngle) { + assertNotInLayoutOrScroll(null); + if (this.minRemoveAngle == minRemoveAngle) return; + this.minRemoveAngle = minRemoveAngle; + requestLayout(); + } + + public void setGravity(int gravity) { + assertNotInLayoutOrScroll(null); + assertGravity(gravity); + if (this.gravity == gravity) return; + this.gravity = gravity; + if (gravity == LEFT || gravity == RIGHT) { + setOrientation(VERTICAL); + } else { + setOrientation(HORIZONTAL); + } + requestLayout(); + } + + public void setFlipRotate(boolean flipRotate) { + assertNotInLayoutOrScroll(null); + if (this.flipRotate == flipRotate) return; + this.flipRotate = flipRotate; + requestLayout(); + } + + public void setZAlignment(int zAlignment) { + assertNotInLayoutOrScroll(null); + assertZAlignmentState(zAlignment); + if (this.zAlignment == zAlignment) return; + this.zAlignment = zAlignment; + requestLayout(); + } + + @Override + protected float setInterval() { + return angleInterval; + } + + @Override + protected void setUp() { + radius = radius == Builder.INVALID_VALUE ? mDecoratedMeasurementInOther : radius; + } + + @Override + protected float maxRemoveOffset() { + return maxRemoveAngle; + } + + @Override + protected float minRemoveOffset() { + return minRemoveAngle; + } + + @Override + protected int calItemLeft(View itemView, float targetOffset) { + switch (gravity) { + case LEFT: + return (int) (radius * Math.sin(Math.toRadians(90 - targetOffset)) - radius); + case RIGHT: + return (int) (radius - radius * Math.sin(Math.toRadians(90 - targetOffset))); + case TOP: + case BOTTOM: + default: + return (int) (radius * Math.cos(Math.toRadians(90 - targetOffset))); + } + } + + @Override + protected int calItemTop(View itemView, float targetOffset) { + switch (gravity) { + case LEFT: + case RIGHT: + return (int) (radius * Math.cos(Math.toRadians(90 - targetOffset))); + case TOP: + return (int) (radius * Math.sin(Math.toRadians(90 - targetOffset)) - radius); + case BOTTOM: + default: + return (int) (radius - radius * Math.sin(Math.toRadians(90 - targetOffset))); + } + } + + @Override + protected void setItemViewProperty(View itemView, float targetOffset) { + float scale = 1f; + switch (gravity) { + case RIGHT: + case TOP: + if (flipRotate) { + itemView.setRotation(targetOffset); + if (targetOffset < angleInterval && targetOffset > -angleInterval) { + float diff = Math.abs(Math.abs(itemView.getRotation() - angleInterval) - angleInterval); + scale = (centerScale - 1f) / -angleInterval * diff + centerScale; + } + } else { + itemView.setRotation(360 - targetOffset); + if (targetOffset < angleInterval && targetOffset > -angleInterval) { + float diff = Math.abs(Math.abs(360 - itemView.getRotation() - angleInterval) - angleInterval); + scale = (centerScale - 1f) / -angleInterval * diff + centerScale; + } + } + break; + case LEFT: + case BOTTOM: + default: + if (flipRotate) { + itemView.setRotation(360 - targetOffset); + if (targetOffset < angleInterval && targetOffset > -angleInterval) { + float diff = Math.abs(Math.abs(360 - itemView.getRotation() - angleInterval) - angleInterval); + scale = (centerScale - 1f) / -angleInterval * diff + centerScale; + } + } else { + itemView.setRotation(targetOffset); + if (targetOffset < angleInterval && targetOffset > -angleInterval) { + float diff = Math.abs(Math.abs(itemView.getRotation() - angleInterval) - angleInterval); + scale = (centerScale - 1f) / -angleInterval * diff + centerScale; + } + } + break; + } + itemView.setScaleX(scale); + itemView.setScaleY(scale); + } + + @Override + protected float setViewElevation(View itemView, float targetOffset) { + if (zAlignment == LEFT_ON_TOP) + return (540 - targetOffset) / 72; + else if (zAlignment == RIGHT_ON_TOP) + return (targetOffset - 540) / 72; + else + return (360 - Math.abs(targetOffset)) / 72; + } + + @Override + protected float getDistanceRatio() { + if (moveSpeed == 0) return Float.MAX_VALUE; + return 1 / moveSpeed; + } + + private static void assertGravity(int gravity) { + if (gravity != LEFT && gravity != RIGHT && gravity != TOP && gravity != BOTTOM) { + throw new IllegalArgumentException("gravity must be one of LEFT RIGHT TOP and BOTTOM"); + } + } + + private static void assertZAlignmentState(int zAlignment) { + if (zAlignment != LEFT_ON_TOP && zAlignment != RIGHT_ON_TOP && zAlignment != CENTER_ON_TOP) { + throw new IllegalArgumentException("zAlignment must be one of LEFT_ON_TOP RIGHT_ON_TOP and CENTER_ON_TOP"); + } + } + + public static class Builder { + private static int INTERVAL_ANGLE = 30;// The default mInterval angle between each items + private static float DISTANCE_RATIO = 10f; // Finger swipe distance divide item rotate angle + private static final float SCALE_RATE = 1.2f; + private static int INVALID_VALUE = Integer.MIN_VALUE; + + private int radius; + private int angleInterval; + private float centerScale; + private float moveSpeed; + private float maxRemoveAngle; + private float minRemoveAngle; + private boolean reverseLayout; + private Context context; + private int gravity; + private boolean flipRotate; + private int zAlignment; + private int maxVisibleItemCount; + private int distanceToBottom; + + public Builder(Context context) { + this.context = context; + radius = INVALID_VALUE; + angleInterval = INTERVAL_ANGLE; + centerScale = SCALE_RATE; + moveSpeed = 1 / DISTANCE_RATIO; + maxRemoveAngle = 90; + minRemoveAngle = -90; + reverseLayout = false; + flipRotate = false; + gravity = BOTTOM; + zAlignment = CENTER_ON_TOP; + distanceToBottom = ViewPagerLayoutManager.INVALID_SIZE; + maxVisibleItemCount = ViewPagerLayoutManager.DETERMINE_BY_MAX_AND_MIN; + } + + public Builder setRadius(int radius) { + this.radius = radius; + return this; + } + + public Builder setAngleInterval(int angleInterval) { + this.angleInterval = angleInterval; + return this; + } + + public Builder setCenterScale(float centerScale) { + this.centerScale = centerScale; + return this; + } + + public Builder setMoveSpeed(int moveSpeed) { + this.moveSpeed = moveSpeed; + return this; + } + + public Builder setMaxRemoveAngle(float maxRemoveAngle) { + this.maxRemoveAngle = maxRemoveAngle; + return this; + } + + public Builder setMinRemoveAngle(float minRemoveAngle) { + this.minRemoveAngle = minRemoveAngle; + return this; + } + + public Builder setReverseLayout(boolean reverseLayout) { + this.reverseLayout = reverseLayout; + return this; + } + + public Builder setGravity(int gravity) { + assertGravity(gravity); + this.gravity = gravity; + return this; + } + + public Builder setFlipRotate(boolean flipRotate) { + this.flipRotate = flipRotate; + return this; + } + + public Builder setZAlignment(int zAlignment) { + assertZAlignmentState(zAlignment); + this.zAlignment = zAlignment; + return this; + } + + public Builder setMaxVisibleItemCount(int maxVisibleItemCount) { + this.maxVisibleItemCount = maxVisibleItemCount; + return this; + } + + public Builder setDistanceToBottom(int distanceToBottom) { + this.distanceToBottom = distanceToBottom; + return this; + } + + public CircleScaleLayoutManager build() { + return new CircleScaleLayoutManager(this); + } + } +} diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/GalleryLayoutManager.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/GalleryLayoutManager.java new file mode 100644 index 0000000..21588d8 --- /dev/null +++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/GalleryLayoutManager.java @@ -0,0 +1,286 @@ +package com.heyongrui.base.widget.layoutmanager; + +import android.content.Context; +import android.view.View; + +/** + * An implementation of {@link ViewPagerLayoutManager} + * which will change rotate x or rotate y + */ + +@SuppressWarnings({"WeakerAccess", "unused"}) +public class GalleryLayoutManager extends ViewPagerLayoutManager { + private final float MAX_ELEVATION = 5F; + + private int itemSpace; + private float moveSpeed; + private float maxAlpha; + private float minAlpha; + private float angle; + private boolean flipRotate; + private boolean rotateFromEdge; + + public GalleryLayoutManager(Context context, int itemSpace) { + this(new Builder(context, itemSpace)); + } + + public GalleryLayoutManager(Context context, int itemSpace, int orientation) { + this(new Builder(context, itemSpace).setOrientation(orientation)); + } + + public GalleryLayoutManager(Context context, int itemSpace, int orientation, boolean reverseLayout) { + this(new Builder(context, itemSpace).setOrientation(orientation).setReverseLayout(reverseLayout)); + } + + public GalleryLayoutManager(Builder builder) { + this(builder.context, builder.itemSpace, builder.angle, builder.maxAlpha, builder.minAlpha, + builder.orientation, builder.moveSpeed, builder.flipRotate, builder.rotateFromEdge, + builder.maxVisibleItemCount, builder.distanceToBottom, builder.reverseLayout); + } + + private GalleryLayoutManager(Context context, int itemSpace, float angle, float maxAlpha, float minAlpha, + int orientation, float moveSpeed, boolean flipRotate, boolean rotateFromEdge, + int maxVisibleItemCount, int distanceToBottom, boolean reverseLayout) { + super(context, orientation, reverseLayout); + setDistanceToBottom(distanceToBottom); + setMaxVisibleItemCount(maxVisibleItemCount); + this.itemSpace = itemSpace; + this.moveSpeed = moveSpeed; + this.angle = angle; + this.maxAlpha = maxAlpha; + this.minAlpha = minAlpha; + this.flipRotate = flipRotate; + this.rotateFromEdge = rotateFromEdge; + } + + public int getItemSpace() { + return itemSpace; + } + + public float getMaxAlpha() { + return maxAlpha; + } + + public float getMinAlpha() { + return minAlpha; + } + + public float getAngle() { + return angle; + } + + public float getMoveSpeed() { + return moveSpeed; + } + + public boolean getFlipRotate() { + return flipRotate; + } + + public boolean getRotateFromEdge() { + return rotateFromEdge; + } + + public void setItemSpace(int itemSpace) { + assertNotInLayoutOrScroll(null); + if (this.itemSpace == itemSpace) return; + this.itemSpace = itemSpace; + removeAllViews(); + } + + public void setMoveSpeed(float moveSpeed) { + assertNotInLayoutOrScroll(null); + if (this.moveSpeed == moveSpeed) return; + this.moveSpeed = moveSpeed; + } + + public void setMaxAlpha(float maxAlpha) { + assertNotInLayoutOrScroll(null); + if (maxAlpha > 1) maxAlpha = 1; + if (this.maxAlpha == maxAlpha) return; + this.maxAlpha = maxAlpha; + requestLayout(); + } + + public void setMinAlpha(float minAlpha) { + assertNotInLayoutOrScroll(null); + if (minAlpha < 0) minAlpha = 0; + if (this.minAlpha == minAlpha) return; + this.minAlpha = minAlpha; + requestLayout(); + } + + public void setAngle(float angle) { + assertNotInLayoutOrScroll(null); + if (this.angle == angle) return; + this.angle = angle; + requestLayout(); + } + + public void setFlipRotate(boolean flipRotate) { + assertNotInLayoutOrScroll(null); + if (this.flipRotate == flipRotate) return; + this.flipRotate = flipRotate; + requestLayout(); + } + + public void setRotateFromEdge(boolean rotateFromEdge) { + assertNotInLayoutOrScroll(null); + if (this.rotateFromEdge == rotateFromEdge) return; + this.rotateFromEdge = rotateFromEdge; + removeAllViews(); + } + + @Override + protected float setInterval() { + return mDecoratedMeasurement + itemSpace; + } + + @Override + protected void setItemViewProperty(View itemView, float targetOffset) { + final float rotation = calRotation(targetOffset); + if (getOrientation() == HORIZONTAL) { + if (rotateFromEdge) { + itemView.setPivotX(rotation > 0 ? 0 : mDecoratedMeasurement); + itemView.setPivotY(mDecoratedMeasurementInOther * 0.5f); + } + if (flipRotate) { + itemView.setRotationX(rotation); + } else { + itemView.setRotationY(rotation); + } + } else { + if (rotateFromEdge) { + itemView.setPivotY(rotation > 0 ? 0 : mDecoratedMeasurement); + itemView.setPivotX(mDecoratedMeasurementInOther * 0.5f); + + } + if (flipRotate) { + itemView.setRotationY(-rotation); + } else { + itemView.setRotationX(-rotation); + } + } + final float alpha = calAlpha(targetOffset); + itemView.setAlpha(alpha); + } + + @Override + protected float setViewElevation(View itemView, float targetOffset) { + final float ele = Math.max(Math.abs(itemView.getRotationX()), Math.abs(itemView.getRotationY())) * MAX_ELEVATION / 360; + return MAX_ELEVATION - ele; + } + + @Override + protected float getDistanceRatio() { + if (moveSpeed == 0) return Float.MAX_VALUE; + return 1 / moveSpeed; + } + + private float calRotation(float targetOffset) { + return -angle / mInterval * targetOffset; + } + + private float calAlpha(float targetOffset) { + final float offset = Math.abs(targetOffset); + float alpha = (minAlpha - maxAlpha) / mInterval * offset + maxAlpha; + if (offset >= mInterval) alpha = minAlpha; + return alpha; + } + + public static class Builder { + private static float INTERVAL_ANGLE = 30f; + private static final float DEFAULT_SPEED = 1f; + private static float MIN_ALPHA = 0.5f; + private static float MAX_ALPHA = 1f; + + private int itemSpace; + private float moveSpeed; + private int orientation; + private float maxAlpha; + private float minAlpha; + private float angle; + private boolean flipRotate; + private boolean reverseLayout; + private Context context; + private int maxVisibleItemCount; + private int distanceToBottom; + private boolean rotateFromEdge; + + public Builder(Context context, int itemSpace) { + this.itemSpace = itemSpace; + this.context = context; + orientation = HORIZONTAL; + angle = INTERVAL_ANGLE; + maxAlpha = MAX_ALPHA; + minAlpha = MIN_ALPHA; + this.moveSpeed = DEFAULT_SPEED; + reverseLayout = false; + flipRotate = false; + rotateFromEdge = false; + distanceToBottom = ViewPagerLayoutManager.INVALID_SIZE; + maxVisibleItemCount = ViewPagerLayoutManager.DETERMINE_BY_MAX_AND_MIN; + } + + public Builder setItemSpace(int itemSpace) { + this.itemSpace = itemSpace; + return this; + } + + public Builder setMoveSpeed(float moveSpeed) { + this.moveSpeed = moveSpeed; + return this; + } + + public Builder setOrientation(int orientation) { + this.orientation = orientation; + return this; + } + + public Builder setMaxAlpha(float maxAlpha) { + if (maxAlpha > 1) maxAlpha = 1; + this.maxAlpha = maxAlpha; + return this; + } + + public Builder setMinAlpha(float minAlpha) { + if (minAlpha < 0) minAlpha = 0; + this.minAlpha = minAlpha; + return this; + } + + public Builder setAngle(float angle) { + this.angle = angle; + return this; + } + + public Builder setFlipRotate(boolean flipRotate) { + this.flipRotate = flipRotate; + return this; + } + + public Builder setReverseLayout(boolean reverseLayout) { + this.reverseLayout = reverseLayout; + return this; + } + + public Builder setMaxVisibleItemCount(int maxVisibleItemCount) { + this.maxVisibleItemCount = maxVisibleItemCount; + return this; + } + + public Builder setDistanceToBottom(int distanceToBottom) { + this.distanceToBottom = distanceToBottom; + return this; + } + + public Builder setRotateFromEdge(boolean rotateFromEdge) { + this.rotateFromEdge = rotateFromEdge; + return this; + } + + public GalleryLayoutManager build() { + return new GalleryLayoutManager(this); + } + } +} diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/OrientationHelper.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/OrientationHelper.java new file mode 100644 index 0000000..08c5b93 --- /dev/null +++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/OrientationHelper.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.heyongrui.base.widget.layoutmanager; + +import android.graphics.Rect; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +/** + * Helper class for LayoutManagers to abstract measurements depending on the View's orientation. + *
+ * It is developed to easily support vertical and horizontal orientations in a LayoutManager but + * can also be used to abstract calls around view bounds and child measurements with margins and + * decorations. + * + * @see #createHorizontalHelper(RecyclerView.LayoutManager) + * @see #createVerticalHelper(RecyclerView.LayoutManager) + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class OrientationHelper { + + private static final int INVALID_SIZE = Integer.MIN_VALUE; + + protected final RecyclerView.LayoutManager mLayoutManager; + + public static final int HORIZONTAL = RecyclerView.HORIZONTAL; + + public static final int VERTICAL = RecyclerView.VERTICAL; + + private int mLastTotalSpace = INVALID_SIZE; + + final Rect mTmpRect = new Rect(); + + private OrientationHelper(RecyclerView.LayoutManager layoutManager) { + mLayoutManager = layoutManager; + } + + /** + * Call this method after onLayout method is complete if state is NOT pre-layout. + * This method records information like layout bounds that might be useful in the next layout + * calculations. + */ + public void onLayoutComplete() { + mLastTotalSpace = getTotalSpace(); + } + + /** + * Returns the layout space change between the previous layout pass and current layout pass. + *
+ * Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's + * {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler, + * RecyclerView.State)} method. + * + * @return The difference between the current total space and previous layout's total space. + * @see #onLayoutComplete() + */ + public int getTotalSpaceChange() { + return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace; + } + + /** + * Returns the start of the view including its decoration and margin. + *
+ * For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left + * decoration and 3px left margin, returned value will be 15px. + * + * @param view The view element to check + * @return The first pixel of the element + * @see #getDecoratedEnd(View) + */ + public abstract int getDecoratedStart(View view); + + /** + * Returns the end of the view including its decoration and margin. + *
+ * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right + * decoration and 3px right margin, returned value will be 205. + * + * @param view The view element to check + * @return The last pixel of the element + * @see #getDecoratedStart(View) + */ + public abstract int getDecoratedEnd(View view); + + /** + * Returns the end of the View after its matrix transformations are applied to its layout + * position. + *
+ * This method is useful when trying to detect the visible edge of a View. + *
+ * It includes the decorations but does not include the margins. + * + * @param view The view whose transformed end will be returned + * @return The end of the View after its decor insets and transformation matrix is applied to + * its position + * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect) + */ + public abstract int getTransformedEndWithDecoration(View view); + + /** + * Returns the start of the View after its matrix transformations are applied to its layout + * position. + *
+ * This method is useful when trying to detect the visible edge of a View. + *
+ * It includes the decorations but does not include the margins.
+ *
+ * @param view The view whose transformed start will be returned
+ * @return The start of the View after its decor insets and transformation matrix is applied to
+ * its position
+ * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect)
+ */
+ public abstract int getTransformedStartWithDecoration(View view);
+
+ /**
+ * Returns the space occupied by this View in the current orientation including decorations and
+ * margins.
+ *
+ * @param view The view element to check
+ * @return Total space occupied by this view
+ * @see #getDecoratedMeasurementInOther(View)
+ */
+ public abstract int getDecoratedMeasurement(View view);
+
+ /**
+ * Returns the space occupied by this View in the perpendicular orientation including
+ * decorations and margins.
+ *
+ * @param view The view element to check
+ * @return Total space occupied by this view in the perpendicular orientation to current one
+ * @see #getDecoratedMeasurement(View)
+ */
+ public abstract int getDecoratedMeasurementInOther(View view);
+
+ /**
+ * Returns the start position of the layout after the start padding is added.
+ *
+ * @return The very first pixel we can draw.
+ */
+ public abstract int getStartAfterPadding();
+
+ /**
+ * Returns the end position of the layout after the end padding is removed.
+ *
+ * @return The end boundary for this layout.
+ */
+ public abstract int getEndAfterPadding();
+
+ /**
+ * Returns the end position of the layout without taking padding into account.
+ *
+ * @return The end boundary for this layout without considering padding.
+ */
+ public abstract int getEnd();
+
+ /**
+ * Returns the total space to layout. This number is the difference between
+ * {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
+ *
+ * @return Total space to layout children
+ */
+ public abstract int getTotalSpace();
+
+ /**
+ * Returns the total space in other direction to layout. This number is the difference between
+ * {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
+ *
+ * @return Total space to layout children
+ */
+ public abstract int getTotalSpaceInOther();
+
+ /**
+ * Returns the padding at the end of the layout. For horizontal helper, this is the right
+ * padding and for vertical helper, this is the bottom padding. This method does not check
+ * whether the layout is RTL or not.
+ *
+ * @return The padding at the end of the layout.
+ */
+ public abstract int getEndPadding();
+
+ /**
+ * Returns the MeasureSpec mode for the current orientation from the LayoutManager.
+ *
+ * @return The current measure spec mode.
+ * @see View.MeasureSpec
+ * @see RecyclerView.LayoutManager#getWidthMode()
+ * @see RecyclerView.LayoutManager#getHeightMode()
+ */
+ public abstract int getMode();
+
+ /**
+ * Returns the MeasureSpec mode for the perpendicular orientation from the LayoutManager.
+ *
+ * @return The current measure spec mode.
+ * @see View.MeasureSpec
+ * @see RecyclerView.LayoutManager#getWidthMode()
+ * @see RecyclerView.LayoutManager#getHeightMode()
+ */
+ public abstract int getModeInOther();
+
+ /**
+ * Creates an OrientationHelper for the given LayoutManager and orientation.
+ *
+ * @param layoutManager LayoutManager to attach to
+ * @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
+ * @return A new OrientationHelper
+ */
+ public static OrientationHelper createOrientationHelper(
+ RecyclerView.LayoutManager layoutManager, int orientation) {
+ switch (orientation) {
+ case HORIZONTAL:
+ return createHorizontalHelper(layoutManager);
+ case VERTICAL:
+ return createVerticalHelper(layoutManager);
+ }
+ throw new IllegalArgumentException("invalid orientation");
+ }
+
+ /**
+ * Creates a horizontal OrientationHelper for the given LayoutManager.
+ *
+ * @param layoutManager The LayoutManager to attach to.
+ * @return A new OrientationHelper
+ */
+ public static OrientationHelper createHorizontalHelper(
+ RecyclerView.LayoutManager layoutManager) {
+ return new OrientationHelper(layoutManager) {
+ @Override
+ public int getEndAfterPadding() {
+ return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
+ }
+
+ @Override
+ public int getEnd() {
+ return mLayoutManager.getWidth();
+ }
+
+ @Override
+ public int getStartAfterPadding() {
+ return mLayoutManager.getPaddingLeft();
+ }
+
+ @Override
+ public int getDecoratedMeasurement(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ + params.rightMargin;
+ }
+
+ @Override
+ public int getDecoratedMeasurementInOther(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ + params.bottomMargin;
+ }
+
+ @Override
+ public int getDecoratedEnd(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
+ }
+
+ @Override
+ public int getDecoratedStart(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
+ }
+
+ @Override
+ public int getTransformedEndWithDecoration(View view) {
+ mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+ return mTmpRect.right;
+ }
+
+ @Override
+ public int getTransformedStartWithDecoration(View view) {
+ mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+ return mTmpRect.left;
+ }
+
+ @Override
+ public int getTotalSpace() {
+ return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
+ - mLayoutManager.getPaddingRight();
+ }
+
+ @Override
+ public int getTotalSpaceInOther() {
+ return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
+ - mLayoutManager.getPaddingBottom();
+ }
+
+ @Override
+ public int getEndPadding() {
+ return mLayoutManager.getPaddingRight();
+ }
+
+ @Override
+ public int getMode() {
+ return mLayoutManager.getWidthMode();
+ }
+
+ @Override
+ public int getModeInOther() {
+ return mLayoutManager.getHeightMode();
+ }
+ };
+ }
+
+ /**
+ * Creates a vertical OrientationHelper for the given LayoutManager.
+ *
+ * @param layoutManager The LayoutManager to attach to.
+ * @return A new OrientationHelper
+ */
+ public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
+ return new OrientationHelper(layoutManager) {
+ @Override
+ public int getEndAfterPadding() {
+ return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
+ }
+
+ @Override
+ public int getEnd() {
+ return mLayoutManager.getHeight();
+ }
+
+ @Override
+ public int getStartAfterPadding() {
+ return mLayoutManager.getPaddingTop();
+ }
+
+ @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 getDecoratedEnd(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
+ }
+
+ @Override
+ public int getDecoratedStart(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedTop(view) - params.topMargin;
+ }
+
+ @Override
+ public int getTransformedEndWithDecoration(View view) {
+ mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+ return mTmpRect.bottom;
+ }
+
+ @Override
+ public int getTransformedStartWithDecoration(View view) {
+ mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
+ return mTmpRect.top;
+ }
+
+ @Override
+ public int getTotalSpace() {
+ return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
+ - mLayoutManager.getPaddingBottom();
+ }
+
+ @Override
+ public int getTotalSpaceInOther() {
+ return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
+ - mLayoutManager.getPaddingRight();
+ }
+
+ @Override
+ public int getEndPadding() {
+ return mLayoutManager.getPaddingBottom();
+ }
+
+ @Override
+ public int getMode() {
+ return mLayoutManager.getHeightMode();
+ }
+
+ @Override
+ public int getModeInOther() {
+ return mLayoutManager.getWidthMode();
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/PageSnapHelper.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/PageSnapHelper.java
new file mode 100644
index 0000000..e7d568a
--- /dev/null
+++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/PageSnapHelper.java
@@ -0,0 +1,52 @@
+package com.heyongrui.base.widget.layoutmanager;
+
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * The implementation will snap the center of the target child view to the center of
+ * the attached {@link RecyclerView}. And per Child per fling.
+ */
+
+public class PageSnapHelper extends CenterSnapHelper {
+
+ @Override
+ public boolean onFling(int velocityX, int velocityY) {
+ ViewPagerLayoutManager layoutManager = (ViewPagerLayoutManager) mRecyclerView.getLayoutManager();
+ if (layoutManager == null) {
+ return false;
+ }
+ RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
+ if (adapter == null) {
+ return false;
+ }
+
+ if (!layoutManager.getInfinite() &&
+ (layoutManager.mOffset == layoutManager.getMaxOffset()
+ || layoutManager.mOffset == layoutManager.getMinOffset())) {
+ return false;
+ }
+
+ final int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
+ mGravityScroller.fling(0, 0, velocityX, velocityY,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
+
+ if (layoutManager.mOrientation == ViewPagerLayoutManager.VERTICAL
+ && Math.abs(velocityY) > minFlingVelocity) {
+ final int currentPosition = layoutManager.getCurrentPositionOffset();
+ final int offsetPosition = mGravityScroller.getFinalY() * layoutManager.getDistanceRatio() > layoutManager.mInterval ? 1 : 0;
+ ScrollHelper.smoothScrollToPosition(mRecyclerView, layoutManager, layoutManager.getReverseLayout() ?
+ -currentPosition - offsetPosition : currentPosition + offsetPosition);
+ return true;
+ } else if (layoutManager.mOrientation == ViewPagerLayoutManager.HORIZONTAL
+ && Math.abs(velocityX) > minFlingVelocity) {
+ final int currentPosition = layoutManager.getCurrentPositionOffset();
+ final int offsetPosition = mGravityScroller.getFinalX() * layoutManager.getDistanceRatio() > layoutManager.mInterval ? 1 : 0;
+ ScrollHelper.smoothScrollToPosition(mRecyclerView, layoutManager, layoutManager.getReverseLayout() ?
+ -currentPosition - offsetPosition : currentPosition + offsetPosition);
+ return true;
+ }
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/RotateLayoutManager.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/RotateLayoutManager.java
new file mode 100644
index 0000000..6e607ef
--- /dev/null
+++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/RotateLayoutManager.java
@@ -0,0 +1,178 @@
+package com.heyongrui.base.widget.layoutmanager;
+
+import android.content.Context;
+import android.view.View;
+
+/**
+ * An implementation of {@link ViewPagerLayoutManager}
+ * which rotates items
+ */
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class RotateLayoutManager extends ViewPagerLayoutManager {
+
+ private int itemSpace;
+ private float angle;
+ private float moveSpeed;
+ private boolean reverseRotate;
+
+ public RotateLayoutManager(Context context, int itemSpace) {
+ this(new Builder(context, itemSpace));
+ }
+
+ public RotateLayoutManager(Context context, int itemSpace, int orientation) {
+ this(new Builder(context, itemSpace).setOrientation(orientation));
+ }
+
+ public RotateLayoutManager(Context context, int itemSpace, int orientation, boolean reverseLayout) {
+ this(new Builder(context, itemSpace).setOrientation(orientation).setReverseLayout(reverseLayout));
+ }
+
+ public RotateLayoutManager(Builder builder) {
+ this(builder.context, builder.itemSpace, builder.angle, builder.orientation, builder.moveSpeed,
+ builder.reverseRotate, builder.maxVisibleItemCount, builder.distanceToBottom,
+ builder.reverseLayout);
+ }
+
+ private RotateLayoutManager(Context context, int itemSpace, float angle, int orientation, float moveSpeed,
+ boolean reverseRotate, int maxVisibleItemCount, int distanceToBottom,
+ boolean reverseLayout) {
+ super(context, orientation, reverseLayout);
+ setDistanceToBottom(distanceToBottom);
+ setMaxVisibleItemCount(maxVisibleItemCount);
+ this.itemSpace = itemSpace;
+ this.angle = angle;
+ this.moveSpeed = moveSpeed;
+ this.reverseRotate = reverseRotate;
+ }
+
+ public int getItemSpace() {
+ return itemSpace;
+ }
+
+ public float getAngle() {
+ return angle;
+ }
+
+ public float getMoveSpeed() {
+ return moveSpeed;
+ }
+
+ public boolean getReverseRotate() {
+ return reverseRotate;
+ }
+
+ public void setItemSpace(int itemSpace) {
+ assertNotInLayoutOrScroll(null);
+ if (this.itemSpace == itemSpace) return;
+ this.itemSpace = itemSpace;
+ removeAllViews();
+ }
+
+ public void setAngle(float centerScale) {
+ assertNotInLayoutOrScroll(null);
+ if (this.angle == centerScale) return;
+ this.angle = centerScale;
+ requestLayout();
+ }
+
+ public void setMoveSpeed(float moveSpeed) {
+ assertNotInLayoutOrScroll(null);
+ if (this.moveSpeed == moveSpeed) return;
+ this.moveSpeed = moveSpeed;
+ }
+
+ public void setReverseRotate(boolean reverseRotate) {
+ assertNotInLayoutOrScroll(null);
+ if (this.reverseRotate == reverseRotate) return;
+ this.reverseRotate = reverseRotate;
+ requestLayout();
+ }
+
+ @Override
+ protected float setInterval() {
+ return mDecoratedMeasurement + itemSpace;
+ }
+
+ @Override
+ protected void setItemViewProperty(View itemView, float targetOffset) {
+ itemView.setRotation(calRotation(targetOffset));
+ }
+
+ @Override
+ protected float getDistanceRatio() {
+ if (moveSpeed == 0) return Float.MAX_VALUE;
+ return 1 / moveSpeed;
+ }
+
+ private float calRotation(float targetOffset) {
+ final float realAngle = reverseRotate ? angle : -angle;
+ return realAngle / mInterval * targetOffset;
+ }
+
+ public static class Builder {
+ private static float INTERVAL_ANGLE = 360f;
+ private static final float DEFAULT_SPEED = 1f;
+
+ private int itemSpace;
+ private int orientation;
+ private float angle;
+ private float moveSpeed;
+ private boolean reverseRotate;
+ private boolean reverseLayout;
+ private Context context;
+ private int maxVisibleItemCount;
+ private int distanceToBottom;
+
+ public Builder(Context context, int itemSpace) {
+ this.context = context;
+ this.itemSpace = itemSpace;
+ orientation = HORIZONTAL;
+ angle = INTERVAL_ANGLE;
+ this.moveSpeed = DEFAULT_SPEED;
+ reverseRotate = false;
+ reverseLayout = false;
+ distanceToBottom = ViewPagerLayoutManager.INVALID_SIZE;
+ maxVisibleItemCount = ViewPagerLayoutManager.DETERMINE_BY_MAX_AND_MIN;
+ }
+
+ public Builder setOrientation(int orientation) {
+ this.orientation = orientation;
+ return this;
+ }
+
+ public Builder setAngle(float angle) {
+ this.angle = angle;
+ return this;
+ }
+
+ public Builder setReverseLayout(boolean reverseLayout) {
+ this.reverseLayout = reverseLayout;
+ return this;
+ }
+
+ public Builder setMoveSpeed(float moveSpeed) {
+ this.moveSpeed = moveSpeed;
+ return this;
+ }
+
+ public Builder setReverseRotate(boolean reverseRotate) {
+ this.reverseRotate = reverseRotate;
+ return this;
+ }
+
+ public Builder setMaxVisibleItemCount(int maxVisibleItemCount) {
+ this.maxVisibleItemCount = maxVisibleItemCount;
+ return this;
+ }
+
+ public Builder setDistanceToBottom(int distanceToBottom) {
+ this.distanceToBottom = distanceToBottom;
+ return this;
+ }
+
+ public RotateLayoutManager build() {
+ return new RotateLayoutManager(this);
+ }
+ }
+}
diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/ScaleLayoutManager.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/ScaleLayoutManager.java
new file mode 100644
index 0000000..ac9a03c
--- /dev/null
+++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/ScaleLayoutManager.java
@@ -0,0 +1,221 @@
+package com.heyongrui.base.widget.layoutmanager;
+
+import android.content.Context;
+import android.view.View;
+
+/**
+ * An implementation of {@link ViewPagerLayoutManager}
+ * which zooms the center item
+ */
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class ScaleLayoutManager extends ViewPagerLayoutManager {
+
+ private int itemSpace;
+ private float minScale;
+ private float moveSpeed;
+ private float maxAlpha;
+ private float minAlpha;
+
+ public ScaleLayoutManager(Context context, int itemSpace) {
+ this(new Builder(context, itemSpace));
+ }
+
+ public ScaleLayoutManager(Context context, int itemSpace, int orientation) {
+ this(new Builder(context, itemSpace).setOrientation(orientation));
+ }
+
+ public ScaleLayoutManager(Context context, int itemSpace, int orientation, boolean reverseLayout) {
+ this(new Builder(context, itemSpace).setOrientation(orientation).setReverseLayout(reverseLayout));
+ }
+
+ public ScaleLayoutManager(Builder builder) {
+ this(builder.context, builder.itemSpace, builder.minScale, builder.maxAlpha, builder.minAlpha,
+ builder.orientation, builder.moveSpeed, builder.maxVisibleItemCount, builder.distanceToBottom,
+ builder.reverseLayout);
+ }
+
+ private ScaleLayoutManager(Context context, int itemSpace, float minScale, float maxAlpha, float minAlpha,
+ int orientation, float moveSpeed, int maxVisibleItemCount, int distanceToBottom,
+ boolean reverseLayout) {
+ super(context, orientation, reverseLayout);
+ setDistanceToBottom(distanceToBottom);
+ setMaxVisibleItemCount(maxVisibleItemCount);
+ this.itemSpace = itemSpace;
+ this.minScale = minScale;
+ this.moveSpeed = moveSpeed;
+ this.maxAlpha = maxAlpha;
+ this.minAlpha = minAlpha;
+ }
+
+ public int getItemSpace() {
+ return itemSpace;
+ }
+
+ public float getMinScale() {
+ return minScale;
+ }
+
+ public float getMoveSpeed() {
+ return moveSpeed;
+ }
+
+ public float getMaxAlpha() {
+ return maxAlpha;
+ }
+
+ public float getMinAlpha() {
+ return minAlpha;
+ }
+
+ public void setItemSpace(int itemSpace) {
+ assertNotInLayoutOrScroll(null);
+ if (this.itemSpace == itemSpace) return;
+ this.itemSpace = itemSpace;
+ removeAllViews();
+ }
+
+ public void setMinScale(float minScale) {
+ assertNotInLayoutOrScroll(null);
+ if (this.minScale == minScale) return;
+ this.minScale = minScale;
+ removeAllViews();
+ }
+
+ public void setMaxAlpha(float maxAlpha) {
+ assertNotInLayoutOrScroll(null);
+ if (maxAlpha > 1) maxAlpha = 1;
+ if (this.maxAlpha == maxAlpha) return;
+ this.maxAlpha = maxAlpha;
+ requestLayout();
+ }
+
+ public void setMinAlpha(float minAlpha) {
+ assertNotInLayoutOrScroll(null);
+ if (minAlpha < 0) minAlpha = 0;
+ if (this.minAlpha == minAlpha) return;
+ this.minAlpha = minAlpha;
+ requestLayout();
+ }
+
+ public void setMoveSpeed(float moveSpeed) {
+ assertNotInLayoutOrScroll(null);
+ if (this.moveSpeed == moveSpeed) return;
+ this.moveSpeed = moveSpeed;
+ }
+
+ @Override
+ protected float setInterval() {
+ return itemSpace + mDecoratedMeasurement;
+ }
+
+ @Override
+ protected void setItemViewProperty(View itemView, float targetOffset) {
+ float scale = calculateScale(targetOffset + mSpaceMain);
+ itemView.setScaleX(scale);
+ itemView.setScaleY(scale);
+ final float alpha = calAlpha(targetOffset);
+ itemView.setAlpha(alpha);
+ }
+
+ private float calAlpha(float targetOffset) {
+ final float offset = Math.abs(targetOffset);
+ float alpha = (minAlpha - maxAlpha) / mInterval * offset + maxAlpha;
+ if (offset >= mInterval) alpha = minAlpha;
+ return alpha;
+ }
+
+ @Override
+ protected float getDistanceRatio() {
+ if (moveSpeed == 0) return Float.MAX_VALUE;
+ return 1 / moveSpeed;
+ }
+
+ /**
+ * @param x start positon of the view you want scale
+ * @return the scale rate of current scroll mOffset
+ */
+ private float calculateScale(float x) {
+ float deltaX = Math.abs(x - mSpaceMain);
+ if (deltaX - mDecoratedMeasurement > 0) deltaX = mDecoratedMeasurement;
+ return 1f - deltaX / mDecoratedMeasurement * (1f - minScale);
+ }
+
+ public static class Builder {
+ private static final float SCALE_RATE = 0.8f;
+ private static final float DEFAULT_SPEED = 1f;
+ private static float MIN_ALPHA = 1f;
+ private static float MAX_ALPHA = 1f;
+
+ private int itemSpace;
+ private int orientation;
+ private float minScale;
+ private float moveSpeed;
+ private float maxAlpha;
+ private float minAlpha;
+ private boolean reverseLayout;
+ private Context context;
+ private int maxVisibleItemCount;
+ private int distanceToBottom;
+
+ public Builder(Context context, int itemSpace) {
+ this.itemSpace = itemSpace;
+ this.context = context;
+ orientation = HORIZONTAL;
+ minScale = SCALE_RATE;
+ this.moveSpeed = DEFAULT_SPEED;
+ maxAlpha = MAX_ALPHA;
+ minAlpha = MIN_ALPHA;
+ reverseLayout = false;
+ distanceToBottom = ViewPagerLayoutManager.INVALID_SIZE;
+ maxVisibleItemCount = ViewPagerLayoutManager.DETERMINE_BY_MAX_AND_MIN;
+ }
+
+ public Builder setOrientation(int orientation) {
+ this.orientation = orientation;
+ return this;
+ }
+
+ public Builder setMinScale(float minScale) {
+ this.minScale = minScale;
+ return this;
+ }
+
+ public Builder setReverseLayout(boolean reverseLayout) {
+ this.reverseLayout = reverseLayout;
+ return this;
+ }
+
+ public Builder setMaxAlpha(float maxAlpha) {
+ if (maxAlpha > 1) maxAlpha = 1;
+ this.maxAlpha = maxAlpha;
+ return this;
+ }
+
+ public Builder setMinAlpha(float minAlpha) {
+ if (minAlpha < 0) minAlpha = 0;
+ this.minAlpha = minAlpha;
+ return this;
+ }
+
+ public Builder setMoveSpeed(float moveSpeed) {
+ this.moveSpeed = moveSpeed;
+ return this;
+ }
+
+ public Builder setMaxVisibleItemCount(int maxVisibleItemCount) {
+ this.maxVisibleItemCount = maxVisibleItemCount;
+ return this;
+ }
+
+ public Builder setDistanceToBottom(int distanceToBottom) {
+ this.distanceToBottom = distanceToBottom;
+ return this;
+ }
+
+ public ScaleLayoutManager build() {
+ return new ScaleLayoutManager(this);
+ }
+ }
+}
+
diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/ScrollHelper.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/ScrollHelper.java
new file mode 100644
index 0000000..e9ce2fd
--- /dev/null
+++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/ScrollHelper.java
@@ -0,0 +1,24 @@
+package com.heyongrui.base.widget.layoutmanager;
+
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+public class ScrollHelper {
+ /* package */
+ static void smoothScrollToPosition(RecyclerView recyclerView, ViewPagerLayoutManager viewPagerLayoutManager, int targetPosition) {
+ final int delta = viewPagerLayoutManager.getOffsetToPosition(targetPosition);
+ if (viewPagerLayoutManager.getOrientation() == ViewPagerLayoutManager.VERTICAL) {
+ recyclerView.smoothScrollBy(0, delta);
+ } else {
+ recyclerView.smoothScrollBy(delta, 0);
+ }
+ }
+
+ public static void smoothScrollToTargetView(RecyclerView recyclerView, View targetView) {
+ final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+ if (!(layoutManager instanceof ViewPagerLayoutManager)) return;
+ final int targetPosition = ((ViewPagerLayoutManager) layoutManager).getLayoutPositionOfView(targetView);
+ smoothScrollToPosition(recyclerView, (ViewPagerLayoutManager) layoutManager, targetPosition);
+ }
+}
diff --git a/base/src/main/java/com/heyongrui/base/widget/layoutmanager/ViewPagerLayoutManager.java b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/ViewPagerLayoutManager.java
new file mode 100644
index 0000000..1b0f296
--- /dev/null
+++ b/base/src/main/java/com/heyongrui/base/widget/layoutmanager/ViewPagerLayoutManager.java
@@ -0,0 +1,971 @@
+package com.heyongrui.base.widget.layoutmanager;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+
+import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+
+
+/**
+ * An implementation of {@link RecyclerView.LayoutManager} which behaves like view pager.
+ * Please make sure your child view have the same size.
+ */
+
+@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"})
+public abstract class ViewPagerLayoutManager extends LinearLayoutManager {
+
+ public static final int DETERMINE_BY_MAX_AND_MIN = -1;
+
+ public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
+
+ public static final int VERTICAL = OrientationHelper.VERTICAL;
+
+ private static final int DIRECTION_NO_WHERE = -1;
+
+ private static final int DIRECTION_FORWARD = 0;
+
+ private static final int DIRECTION_BACKWARD = 1;
+
+ protected static final int INVALID_SIZE = Integer.MAX_VALUE;
+
+ private SparseArray
+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
+ * this flag to
+ * Note that, setting this flag will result in a performance drop if RecyclerView
+ * is restored.
+ *
+ * @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
+ */
+ public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
+ mRecycleChildrenOnDetach = recycleChildrenOnDetach;
+ }
+
+ @Override
+ public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
+ super.onDetachedFromWindow(view, recycler);
+ if (mRecycleChildrenOnDetach) {
+ removeAndRecycleAllViews(recycler);
+ recycler.clear();
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mPendingSavedState != null) {
+ return new SavedState(mPendingSavedState);
+ }
+ SavedState savedState = new SavedState();
+ savedState.position = mPendingScrollPosition;
+ savedState.offset = mOffset;
+ savedState.isReverseLayout = mShouldReverseLayout;
+ return savedState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ mPendingSavedState = new SavedState((SavedState) state);
+ requestLayout();
+ }
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #HORIZONTAL}
+ */
+ @Override
+ public boolean canScrollHorizontally() {
+ return mOrientation == HORIZONTAL;
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #VERTICAL}
+ */
+ @Override
+ public boolean canScrollVertically() {
+ return mOrientation == VERTICAL;
+ }
+
+ /**
+ * Returns the current orientation of the layout.
+ *
+ * @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL}
+ * @see #setOrientation(int)
+ */
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ /**
+ * Sets the orientation of the layout. {@link ViewPagerLayoutManager}
+ * will do its best to keep scroll position.
+ *
+ * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL && orientation != VERTICAL) {
+ throw new IllegalArgumentException("invalid orientation:" + orientation);
+ }
+ assertNotInLayoutOrScroll(null);
+ if (orientation == mOrientation) {
+ return;
+ }
+ mOrientation = orientation;
+ mOrientationHelper = null;
+ mDistanceToBottom = INVALID_SIZE;
+ removeAllViews();
+ }
+
+ /**
+ * Returns the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now
+ * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range
+ *
+ * @return Max visible item count
+ */
+ public int getMaxVisibleItemCount() {
+ return mMaxVisibleItemCount;
+ }
+
+ /**
+ * Set the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now
+ * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range
+ *
+ * @param mMaxVisibleItemCount Max visible item count
+ */
+ public void setMaxVisibleItemCount(int mMaxVisibleItemCount) {
+ assertNotInLayoutOrScroll(null);
+ if (this.mMaxVisibleItemCount == mMaxVisibleItemCount) return;
+ this.mMaxVisibleItemCount = mMaxVisibleItemCount;
+ removeAllViews();
+ }
+
+ /**
+ * Calculates the view layout order. (e.g. from end to start or start to end)
+ * RTL layout support is applied automatically. So if layout is RTL and
+ * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
+ */
+ private void resolveShouldLayoutReverse() {
+ // A == B is the same result, but we rather keep it readable
+ if (mOrientation == VERTICAL || !isLayoutRTL()) {
+ mShouldReverseLayout = mReverseLayout;
+ } else {
+ mShouldReverseLayout = !mReverseLayout;
+ }
+ }
+
+ /**
+ * Returns if views are laid out from the opposite direction of the layout.
+ *
+ * @return If layout is reversed or not.
+ * @see #setReverseLayout(boolean)
+ */
+ public boolean getReverseLayout() {
+ return mReverseLayout;
+ }
+
+ /**
+ * Used to reverse item traversal and layout order.
+ * This behaves similar to the layout change for RTL views. When set to true, first item is
+ * laid out at the end of the UI, second item is laid out before it etc.
+ *
+ * For horizontal layouts, it depends on the layout direction.
+ * from LTR.
+ */
+ public void setReverseLayout(boolean reverseLayout) {
+ assertNotInLayoutOrScroll(null);
+ if (reverseLayout == mReverseLayout) {
+ return;
+ }
+ mReverseLayout = reverseLayout;
+ removeAllViews();
+ }
+
+ public void setSmoothScrollInterpolator(Interpolator smoothScrollInterpolator) {
+ this.mSmoothScrollInterpolator = smoothScrollInterpolator;
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
+ final int offsetPosition;
+
+ // fix wrong scroll direction when infinite enable
+ if (mInfinite) {
+ final int currentPosition = getCurrentPosition();
+ final int total = getItemCount();
+ final int targetPosition;
+ if (position < currentPosition) {
+ int d1 = currentPosition - position;
+ int d2 = total - currentPosition + position;
+ targetPosition = d1 < d2 ? (currentPosition - d1) : (currentPosition + d2);
+ } else {
+ int d1 = position - currentPosition;
+ int d2 = currentPosition + total - position;
+ targetPosition = d1 < d2 ? (currentPosition + d1) : (currentPosition - d2);
+ }
+
+ offsetPosition = getOffsetToPosition(targetPosition);
+ } else {
+ offsetPosition = getOffsetToPosition(position);
+ }
+
+ if (mOrientation == VERTICAL) {
+ recyclerView.smoothScrollBy(0, offsetPosition, mSmoothScrollInterpolator);
+ } else {
+ recyclerView.smoothScrollBy(offsetPosition, 0, mSmoothScrollInterpolator);
+ }
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (state.getItemCount() == 0) {
+ removeAndRecycleAllViews(recycler);
+ mOffset = 0;
+ return;
+ }
+
+ ensureLayoutState();
+ resolveShouldLayoutReverse();
+
+ //make sure properties are correct while measure more than once
+ View scrap = getMeasureView(recycler, state, 0);
+ if (scrap == null) {
+ removeAndRecycleAllViews(recycler);
+ mOffset = 0;
+ return;
+ }
+
+ measureChildWithMargins(scrap, 0, 0);
+ mDecoratedMeasurement = mOrientationHelper.getDecoratedMeasurement(scrap);
+ mDecoratedMeasurementInOther = mOrientationHelper.getDecoratedMeasurementInOther(scrap);
+ mSpaceMain = (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2;
+ if (mDistanceToBottom == INVALID_SIZE) {
+ mSpaceInOther = (mOrientationHelper.getTotalSpaceInOther() - mDecoratedMeasurementInOther) / 2;
+ } else {
+ mSpaceInOther = mOrientationHelper.getTotalSpaceInOther() - mDecoratedMeasurementInOther - mDistanceToBottom;
+ }
+
+ mInterval = setInterval();
+ setUp();
+ if (mInterval == 0) {
+ mLeftItems = 1;
+ mRightItems = 1;
+ } else {
+ mLeftItems = (int) Math.abs(minRemoveOffset() / mInterval) + 1;
+ mRightItems = (int) Math.abs(maxRemoveOffset() / mInterval) + 1;
+ }
+
+ if (mPendingSavedState != null) {
+ mShouldReverseLayout = mPendingSavedState.isReverseLayout;
+ mPendingScrollPosition = mPendingSavedState.position;
+ mOffset = mPendingSavedState.offset;
+ }
+
+ if (mPendingScrollPosition != NO_POSITION) {
+ mOffset = mShouldReverseLayout ?
+ mPendingScrollPosition * -mInterval : mPendingScrollPosition * mInterval;
+ }
+
+ layoutItems(recycler);
+ }
+
+ private View getMeasureView(RecyclerView.Recycler recycler, RecyclerView.State state, int index) {
+ if (index >= state.getItemCount() || index < 0) return null;
+ try {
+ return recycler.getViewForPosition(index);
+ } catch (Exception e) {
+ return getMeasureView(recycler, state, index + 1);
+ }
+ }
+
+ @Override
+ public void onLayoutCompleted(RecyclerView.State state) {
+ super.onLayoutCompleted(state);
+ mPendingSavedState = null;
+ mPendingScrollPosition = NO_POSITION;
+ }
+
+ @Override
+ public boolean onAddFocusables(RecyclerView recyclerView, ArrayList
+ * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based
+ * solely on the number of items in the adapter and the position of the visible items inside
+ * the adapter. This provides a stable scrollbar as the user navigates through a list of items
+ * with varying widths / heights.
+ *
+ * @param enabled Whether or not to enable smooth scrollbar.
+ * @see #setSmoothScrollbarEnabled(boolean)
+ */
+ public void setSmoothScrollbarEnabled(boolean enabled) {
+ mSmoothScrollbarEnabled = enabled;
+ }
+
+ public void setEnableBringCenterToFront(boolean bringCenterToTop) {
+ assertNotInLayoutOrScroll(null);
+ if (mEnableBringCenterToFront == bringCenterToTop) {
+ return;
+ }
+ this.mEnableBringCenterToFront = bringCenterToTop;
+ requestLayout();
+ }
+
+ public boolean getEnableBringCenterToFront() {
+ return mEnableBringCenterToFront;
+ }
+
+ /**
+ * Returns the current state of the smooth scrollbar feature. It is enabled by default.
+ *
+ * @return True if smooth scrollbar is enabled, false otherwise.
+ * @see #setSmoothScrollbarEnabled(boolean)
+ */
+ public boolean getSmoothScrollbarEnabled() {
+ return mSmoothScrollbarEnabled;
+ }
+
+ private static class SavedState implements Parcelable {
+ int position;
+ float offset;
+ boolean isReverseLayout;
+
+ SavedState() {
+
+ }
+
+ SavedState(Parcel in) {
+ position = in.readInt();
+ offset = in.readFloat();
+ isReverseLayout = in.readInt() == 1;
+ }
+
+ public SavedState(SavedState other) {
+ position = other.position;
+ offset = other.offset;
+ isReverseLayout = other.isReverseLayout;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(position);
+ dest.writeFloat(offset);
+ dest.writeInt(isReverseLayout ? 1 : 0);
+ }
+
+ public static final Creatortrue
so that views will be available to other RecyclerViews
+ * immediately.
+ *