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 positionCache = new SparseArray<>(); + + protected int mDecoratedMeasurement; + + protected int mDecoratedMeasurementInOther; + + /** + * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} + */ + int mOrientation; + + protected int mSpaceMain; + + protected int mSpaceInOther; + + /** + * The offset of property which will change while scrolling + */ + protected float mOffset; + + /** + * Many calculations are made depending on orientation. To keep it clean, this interface + * helps {@link LinearLayoutManager} make those decisions. + * Based on {@link #mOrientation}, an implementation is lazily created in + * {@link #ensureLayoutState} method. + */ + protected OrientationHelper mOrientationHelper; + + /** + * Defines if layout should be calculated from end to start. + */ + private boolean mReverseLayout = false; + + /** + * This keeps the final value for how LayoutManager should start laying out views. + * It is calculated by checking {@link #getReverseLayout()} and View's layout direction. + * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run. + */ + private boolean mShouldReverseLayout = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. + * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} + */ + private boolean mSmoothScrollbarEnabled = true; + + /** + * When LayoutManager needs to scroll to a position, it sets this variable and requests a + * layout which will check this variable and re-layout accordingly. + */ + private int mPendingScrollPosition = NO_POSITION; + + private SavedState mPendingSavedState = null; + + protected float mInterval; //the mInterval of each item's mOffset + + /* package */ OnPageChangeListener onPageChangeListener; + + private boolean mRecycleChildrenOnDetach; + + private boolean mInfinite = false; + + private boolean mEnableBringCenterToFront; + + private int mLeftItems; + + private int mRightItems; + + /** + * max visible item count + */ + private int mMaxVisibleItemCount = DETERMINE_BY_MAX_AND_MIN; + + private Interpolator mSmoothScrollInterpolator; + + private int mDistanceToBottom = INVALID_SIZE; + + /** + * use for handle focus + */ + private View currentFocusView; + + /** + * @return the mInterval of each item's mOffset + */ + protected abstract float setInterval(); + + protected abstract void setItemViewProperty(View itemView, float targetOffset); + + /** + * cause elevation is not support below api 21, + * so you can set your elevation here for supporting it below api 21 + * or you can just setElevation in {@link #setItemViewProperty(View, float)} + */ + protected float setViewElevation(View itemView, float targetOffset) { + return 0; + } + + /** + * Creates a horizontal ViewPagerLayoutManager + */ + public ViewPagerLayoutManager(Context context) { + this(context, HORIZONTAL, false); + } + + /** + * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL} + * @param reverseLayout When set to true, layouts from end to start + */ + public ViewPagerLayoutManager(Context context, int orientation, boolean reverseLayout) { + super(context); + setOrientation(orientation); + setReverseLayout(reverseLayout); + setAutoMeasureEnabled(true); + setItemPrefetchEnabled(false); + } + + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + /** + * Returns whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + * + * @return true if LayoutManager will recycle its children when it is detached from + * RecyclerView. + */ + public boolean getRecycleChildrenOnDetach() { + return mRecycleChildrenOnDetach; + } + + /** + * Set whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + *

+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set + * this flag to true so that views will be available to other RecyclerViews + * immediately. + *

+ * 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 views, int direction, int focusableMode) { + final int currentPosition = getCurrentPosition(); + final View currentView = findViewByPosition(currentPosition); + if (currentView == null) return true; + if (recyclerView.hasFocus()) { + final int movement = getMovement(direction); + if (movement != DIRECTION_NO_WHERE) { + final int targetPosition = movement == DIRECTION_BACKWARD ? + currentPosition - 1 : currentPosition + 1; + ScrollHelper.smoothScrollToPosition(recyclerView, this, targetPosition); + } + } else { + currentView.addFocusables(views, direction, focusableMode); + } + return true; + } + + @Override + public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state) { + return null; + } + + private int getMovement(int direction) { + if (mOrientation == VERTICAL) { + if (direction == View.FOCUS_UP) { + return mShouldReverseLayout ? DIRECTION_FORWARD : DIRECTION_BACKWARD; + } else if (direction == View.FOCUS_DOWN) { + return mShouldReverseLayout ? DIRECTION_BACKWARD : DIRECTION_FORWARD; + } else { + return DIRECTION_NO_WHERE; + } + } else { + if (direction == View.FOCUS_LEFT) { + return mShouldReverseLayout ? DIRECTION_FORWARD : DIRECTION_BACKWARD; + } else if (direction == View.FOCUS_RIGHT) { + return mShouldReverseLayout ? DIRECTION_BACKWARD : DIRECTION_FORWARD; + } else { + return DIRECTION_NO_WHERE; + } + } + } + + void ensureLayoutState() { + if (mOrientationHelper == null) { + mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); + } + } + + /** + * You can set up your own properties here or change the exist properties like mSpaceMain and mSpaceInOther + */ + protected void setUp() { + + } + + private float getProperty(int position) { + return mShouldReverseLayout ? position * -mInterval : position * mInterval; + } + + @Override + public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { + removeAllViews(); + mOffset = 0; + } + + @Override + public void scrollToPosition(int position) { + if (!mInfinite && (position < 0 || position >= getItemCount())) return; + mPendingScrollPosition = position; + mOffset = mShouldReverseLayout ? position * -mInterval : position * mInterval; + requestLayout(); + } + + @Override + public int computeHorizontalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(); + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(); + } + + @Override + public int computeHorizontalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(); + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(); + } + + @Override + public int computeHorizontalScrollRange(RecyclerView.State state) { + return computeScrollRange(); + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + return computeScrollRange(); + } + + private int computeScrollOffset() { + if (getChildCount() == 0) { + return 0; + } + + if (!mSmoothScrollbarEnabled) { + return !mShouldReverseLayout ? + getCurrentPosition() : getItemCount() - getCurrentPosition() - 1; + } + + final float realOffset = getOffsetOfRightAdapterPosition(); + return !mShouldReverseLayout ? (int) realOffset : (int) ((getItemCount() - 1) * mInterval + realOffset); + } + + private int computeScrollExtent() { + if (getChildCount() == 0) { + return 0; + } + + if (!mSmoothScrollbarEnabled) { + return 1; + } + + return (int) mInterval; + } + + private int computeScrollRange() { + if (getChildCount() == 0) { + return 0; + } + + if (!mSmoothScrollbarEnabled) { + return getItemCount(); + } + + return (int) (getItemCount() * mInterval); + } + + @Override + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (mOrientation == VERTICAL) { + return 0; + } + return scrollBy(dx, recycler, state); + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (mOrientation == HORIZONTAL) { + return 0; + } + return scrollBy(dy, recycler, state); + } + + private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0 || dy == 0) { + return 0; + } + ensureLayoutState(); + int willScroll = dy; + + float realDx = dy / getDistanceRatio(); + if (Math.abs(realDx) < 0.00000001f) { + return 0; + } + float targetOffset = mOffset + realDx; + + //handle the boundary + if (!mInfinite && targetOffset < getMinOffset()) { + willScroll -= (targetOffset - getMinOffset()) * getDistanceRatio(); + } else if (!mInfinite && targetOffset > getMaxOffset()) { + willScroll = (int) ((getMaxOffset() - mOffset) * getDistanceRatio()); + } + + realDx = willScroll / getDistanceRatio(); + + mOffset += realDx; + + //handle recycle + layoutItems(recycler); + + return willScroll; + } + + private void layoutItems(RecyclerView.Recycler recycler) { + detachAndScrapAttachedViews(recycler); + positionCache.clear(); + + final int itemCount = getItemCount(); + if (itemCount == 0) return; + + // make sure that current position start from 0 to 1 + final int currentPos = mShouldReverseLayout ? + -getCurrentPositionOffset() : getCurrentPositionOffset(); + int start = currentPos - mLeftItems; + int end = currentPos + mRightItems; + + // handle max visible count + if (useMaxVisibleCount()) { + boolean isEven = mMaxVisibleItemCount % 2 == 0; + if (isEven) { + int offset = mMaxVisibleItemCount / 2; + start = currentPos - offset + 1; + end = currentPos + offset + 1; + } else { + int offset = (mMaxVisibleItemCount - 1) / 2; + start = currentPos - offset; + end = currentPos + offset + 1; + } + } + + if (!mInfinite) { + if (start < 0) { + start = 0; + if (useMaxVisibleCount()) end = mMaxVisibleItemCount; + } + if (end > itemCount) end = itemCount; + } + + float lastOrderWeight = Float.MIN_VALUE; + for (int i = start; i < end; i++) { + if (useMaxVisibleCount() || !removeCondition(getProperty(i) - mOffset)) { + // start and end base on current position, + // so we need to calculate the adapter position + int adapterPosition = i; + if (i >= itemCount) { + adapterPosition %= itemCount; + } else if (i < 0) { + int delta = (-adapterPosition) % itemCount; + if (delta == 0) delta = itemCount; + adapterPosition = itemCount - delta; + } + final View scrap = recycler.getViewForPosition(adapterPosition); + measureChildWithMargins(scrap, 0, 0); + resetViewProperty(scrap); + // we need i to calculate the real offset of current view + final float targetOffset = getProperty(i) - mOffset; + layoutScrap(scrap, targetOffset); + final float orderWeight = mEnableBringCenterToFront ? + setViewElevation(scrap, targetOffset) : adapterPosition; + if (orderWeight > lastOrderWeight) { + addView(scrap); + } else { + addView(scrap, 0); + } + if (i == currentPos) currentFocusView = scrap; + lastOrderWeight = orderWeight; + positionCache.put(i, scrap); + } + } + + currentFocusView.requestFocus(); + } + + private boolean useMaxVisibleCount() { + return mMaxVisibleItemCount != DETERMINE_BY_MAX_AND_MIN; + } + + private boolean removeCondition(float targetOffset) { + return targetOffset > maxRemoveOffset() || targetOffset < minRemoveOffset(); + } + + private void resetViewProperty(View v) { + v.setRotation(0); + v.setRotationY(0); + v.setRotationX(0); + v.setScaleX(1f); + v.setScaleY(1f); + v.setAlpha(1f); + } + + /* package */ float getMaxOffset() { + return !mShouldReverseLayout ? (getItemCount() - 1) * mInterval : 0; + } + + /* package */ float getMinOffset() { + return !mShouldReverseLayout ? 0 : -(getItemCount() - 1) * mInterval; + } + + private void layoutScrap(View scrap, float targetOffset) { + final int left = calItemLeft(scrap, targetOffset); + final int top = calItemTop(scrap, targetOffset); + if (mOrientation == VERTICAL) { + layoutDecorated(scrap, mSpaceInOther + left, mSpaceMain + top, + mSpaceInOther + left + mDecoratedMeasurementInOther, mSpaceMain + top + mDecoratedMeasurement); + } else { + layoutDecorated(scrap, mSpaceMain + left, mSpaceInOther + top, + mSpaceMain + left + mDecoratedMeasurement, mSpaceInOther + top + mDecoratedMeasurementInOther); + } + setItemViewProperty(scrap, targetOffset); + } + + protected int calItemLeft(View itemView, float targetOffset) { + return mOrientation == VERTICAL ? 0 : (int) targetOffset; + } + + protected int calItemTop(View itemView, float targetOffset) { + return mOrientation == VERTICAL ? (int) targetOffset : 0; + } + + /** + * when the target offset reach this, + * the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)} + */ + protected float maxRemoveOffset() { + return mOrientationHelper.getTotalSpace() - mSpaceMain; + } + + /** + * when the target offset reach this, + * the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)} + */ + protected float minRemoveOffset() { + return -mDecoratedMeasurement - mOrientationHelper.getStartAfterPadding() - mSpaceMain; + } + + protected float getDistanceRatio() { + return 1f; + } + + public int getCurrentPosition() { + if (getItemCount() == 0) return 0; + + int position = getCurrentPositionOffset(); + if (!mInfinite) return Math.abs(position); + + position = !mShouldReverseLayout ? + //take care of position = getItemCount() + (position >= 0 ? + position % getItemCount() : + getItemCount() + position % getItemCount()) : + (position > 0 ? + getItemCount() - position % getItemCount() : + -position % getItemCount()); + return position == getItemCount() ? 0 : position; + } + + @Override + public View findViewByPosition(int position) { + final int itemCount = getItemCount(); + if (itemCount == 0) return null; + for (int i = 0; i < positionCache.size(); i++) { + final int key = positionCache.keyAt(i); + if (key >= 0) { + if (position == key % itemCount) return positionCache.valueAt(i); + } else { + int delta = key % itemCount; + if (delta == 0) delta = -itemCount; + if (itemCount + delta == position) return positionCache.valueAt(i); + } + } + return null; + } + + public int getLayoutPositionOfView(View v) { + for (int i = 0; i < positionCache.size(); i++) { + int key = positionCache.keyAt(i); + View value = positionCache.get(key); + if (value == v) return key; + } + return -1; + } + + /* package */ int getCurrentPositionOffset() { + if (mInterval == 0) return 0; + return Math.round(mOffset / mInterval); + } + + /** + * Sometimes we need to get the right offset of matching adapter position + * cause when {@link #mInfinite} is set true, there will be no limitation of {@link #mOffset} + */ + private float getOffsetOfRightAdapterPosition() { + if (mShouldReverseLayout) + return mInfinite ? + (mOffset <= 0 ? + (mOffset % (mInterval * getItemCount())) : + (getItemCount() * -mInterval + mOffset % (mInterval * getItemCount()))) : + mOffset; + else + return mInfinite ? + (mOffset >= 0 ? + (mOffset % (mInterval * getItemCount())) : + (getItemCount() * mInterval + mOffset % (mInterval * getItemCount()))) : + mOffset; + } + + /** + * used by {@link CenterSnapHelper} to center the current view + * + * @return the dy between center and current position + */ + public int getOffsetToCenter() { + if (mInfinite) + return (int) ((getCurrentPositionOffset() * mInterval - mOffset) * getDistanceRatio()); + return (int) ((getCurrentPosition() * + (!mShouldReverseLayout ? mInterval : -mInterval) - mOffset) * getDistanceRatio()); + } + + public int getOffsetToPosition(int position) { + if (mInfinite) + return (int) (((getCurrentPositionOffset() + + (!mShouldReverseLayout ? position - getCurrentPositionOffset() : -getCurrentPositionOffset() - position)) * + mInterval - mOffset) * getDistanceRatio()); + return (int) ((position * + (!mShouldReverseLayout ? mInterval : -mInterval) - mOffset) * getDistanceRatio()); + } + + public void setOnPageChangeListener(OnPageChangeListener onPageChangeListener) { + this.onPageChangeListener = onPageChangeListener; + } + + public void setInfinite(boolean enable) { + assertNotInLayoutOrScroll(null); + if (enable == mInfinite) { + return; + } + mInfinite = enable; + requestLayout(); + } + + public boolean getInfinite() { + return mInfinite; + } + + public int getDistanceToBottom() { + return mDistanceToBottom == INVALID_SIZE ? + (mOrientationHelper.getTotalSpaceInOther() - mDecoratedMeasurementInOther) / 2 : mDistanceToBottom; + } + + public void setDistanceToBottom(int mDistanceToBottom) { + assertNotInLayoutOrScroll(null); + if (this.mDistanceToBottom == mDistanceToBottom) return; + this.mDistanceToBottom = mDistanceToBottom; + removeAllViews(); + } + + /** + * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed + * based on the number of visible pixels in the visible items. This however assumes that all + * list items have similar or equal widths or heights (depending on list orientation). + * If you use a list in which items have different dimensions, the scrollbar will change + * appearance as the user scrolls through the list. To avoid this issue, you need to disable + * this property. + *

+ * 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 Creator CREATOR + = new Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public interface OnPageChangeListener { + void onPageSelected(int position); + + void onPageScrollStateChanged(int state); + } +}