From 2690d65d4b107f25e97583f604d5ba57ee6b1440 Mon Sep 17 00:00:00 2001 From: Arman Kolahan Date: Sun, 27 Dec 2020 15:02:58 +0100 Subject: [PATCH] migrate from java to kotlin --- build.gradle | 4 +- card-slider/build.gradle | 18 +- .../cardslider/CardSliderLayoutManager.java | 720 ------------------ .../cardslider/CardSliderLayoutManager.kt | 620 +++++++++++++++ .../ramotion/cardslider/CardSnapHelper.java | 140 ---- .../com/ramotion/cardslider/CardSnapHelper.kt | 108 +++ .../cardslider/DefaultViewUpdater.java | 129 ---- .../ramotion/cardslider/DefaultViewUpdater.kt | 109 +++ gradle/wrapper/gradle-wrapper.properties | 2 +- 9 files changed, 853 insertions(+), 997 deletions(-) delete mode 100644 card-slider/src/main/java/com/ramotion/cardslider/CardSliderLayoutManager.java create mode 100644 card-slider/src/main/java/com/ramotion/cardslider/CardSliderLayoutManager.kt delete mode 100644 card-slider/src/main/java/com/ramotion/cardslider/CardSnapHelper.java create mode 100644 card-slider/src/main/java/com/ramotion/cardslider/CardSnapHelper.kt delete mode 100644 card-slider/src/main/java/com/ramotion/cardslider/DefaultViewUpdater.java create mode 100644 card-slider/src/main/java/com/ramotion/cardslider/DefaultViewUpdater.kt diff --git a/build.gradle b/build.gradle index 2e3cbfb..87137aa 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,13 @@ buildscript { + ext.kotlin_version = '1.4.21' repositories { jcenter() google() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.1' + classpath 'com.android.tools.build:gradle:4.1.1' classpath 'com.bmuschko:gradle-nexus-plugin:2.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/card-slider/build.gradle b/card-slider/build.gradle index 7d79740..45fb645 100644 --- a/card-slider/build.gradle +++ b/card-slider/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' apply plugin: 'signing' apply plugin: 'com.bmuschko.nexus' @@ -26,14 +27,16 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'com.google.android.material:material:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.google.android.material:material:1.2.1' + implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.cardview:cardview:1.0.0' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + testImplementation 'junit:junit:4.13.1' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation "androidx.core:core-ktx:1.3.2" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } modifyPom { @@ -77,3 +80,6 @@ nexus { repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' } +repositories { + mavenCentral() +} diff --git a/card-slider/src/main/java/com/ramotion/cardslider/CardSliderLayoutManager.java b/card-slider/src/main/java/com/ramotion/cardslider/CardSliderLayoutManager.java deleted file mode 100644 index e36c304..0000000 --- a/card-slider/src/main/java/com/ramotion/cardslider/CardSliderLayoutManager.java +++ /dev/null @@ -1,720 +0,0 @@ -package com.ramotion.cardslider; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.PointF; -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.LinearSmoothScroller; -import androidx.recyclerview.widget.RecyclerView; - -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.util.SparseArray; -import android.util.SparseIntArray; -import android.view.View; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.LinkedList; - -/** - * A {@link androidx.recyclerview.widget.RecyclerView.LayoutManager} implementation. - */ -public class CardSliderLayoutManager extends RecyclerView.LayoutManager - implements RecyclerView.SmoothScroller.ScrollVectorProvider { - - private static final int DEFAULT_ACTIVE_CARD_LEFT_OFFSET = 50; - private static final int DEFAULT_CARD_WIDTH = 148; - private static final int DEFAULT_CARDS_GAP = 12; - private static final int LEFT_CARD_COUNT = 2; - - private final SparseArray viewCache = new SparseArray<>(); - private final SparseIntArray cardsXCoords = new SparseIntArray(); - - private int cardWidth; - private int activeCardLeft; - private int activeCardRight; - private int activeCardCenter; - - private float cardsGap; - - private int scrollRequestedPosition = 0; - - private ViewUpdater viewUpdater; - private RecyclerView recyclerView; - - /** - * A ViewUpdater is invoked whenever a visible/attached card is scrolled. - */ - public interface ViewUpdater { - /** - * Called when CardSliderLayoutManager initialized - */ - void onLayoutManagerInitialized(@NonNull CardSliderLayoutManager lm); - - /** - * Called on view update (scroll, layout). - * @param view Updating view - * @param position Position of card relative to the current active card position of the layout manager. - * 0 is active card. 1 is first right card, and -1 is first left (stacked) card. - */ - void updateView(@NonNull View view, float position); - } - - private static class SavedState implements Parcelable { - - int anchorPos; - - SavedState() { - - } - - SavedState(Parcel in) { - anchorPos = in.readInt(); - } - - public SavedState(SavedState other) { - anchorPos = other.anchorPos; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int i) { - parcel.writeInt(anchorPos); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public SavedState createFromParcel(Parcel parcel) { - return new SavedState(parcel); - } - - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - - } - - /** - * Creates CardSliderLayoutManager with default values - * - * @param context Current context, will be used to access resources. - */ - public CardSliderLayoutManager(@NonNull Context context) { - this(context, null, 0, 0); - } - - /** - * Constructor used when layout manager is set in XML by RecyclerView attribute - * "layoutManager". - * - * See {@link R.styleable#CardSlider_activeCardLeftOffset} - * See {@link R.styleable#CardSlider_cardWidth} - * See {@link R.styleable#CardSlider_cardsGap} - */ - public CardSliderLayoutManager(@NonNull Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - final float density = context.getResources().getDisplayMetrics().density; - - final int defaultCardWidth = (int) (DEFAULT_CARD_WIDTH * density); - final int defaultActiveCardLeft = (int) (DEFAULT_ACTIVE_CARD_LEFT_OFFSET * density); - final float defaultCardsGap = DEFAULT_CARDS_GAP * density; - - if (attrs == null) { - initialize(defaultActiveCardLeft, defaultCardWidth, defaultCardsGap, null); - } else { - int attrCardWidth; - int attrActiveCardLeft; - float attrCardsGap; - String viewUpdateClassName; - - final TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CardSlider, 0, 0); - try { - attrCardWidth = a.getDimensionPixelSize(R.styleable.CardSlider_cardWidth, defaultCardWidth); - attrActiveCardLeft = a.getDimensionPixelSize(R.styleable.CardSlider_activeCardLeftOffset, defaultActiveCardLeft); - attrCardsGap = a.getDimension(R.styleable.CardSlider_cardsGap, defaultCardsGap); - viewUpdateClassName = a.getString(R.styleable.CardSlider_viewUpdater); - } finally { - a.recycle(); - } - - final ViewUpdater viewUpdater = loadViewUpdater(context, viewUpdateClassName, attrs); - initialize(attrActiveCardLeft, attrCardWidth, attrCardsGap, viewUpdater); - } - } - - /** - * Creates CardSliderLayoutManager with specified values in pixels. - * - * @param activeCardLeft Active card offset from start of RecyclerView. Default value is 50dp. - * @param cardWidth Card width. Default value is 148dp. - * @param cardsGap Distance between cards. Default value is 12dp. - */ - public CardSliderLayoutManager(int activeCardLeft, int cardWidth, float cardsGap) { - initialize(activeCardLeft, cardWidth, cardsGap, null); - } - - private void initialize(int left, int width, float gap, @Nullable ViewUpdater updater) { - this.cardWidth = width; - this.activeCardLeft = left; - this.activeCardRight = activeCardLeft + cardWidth; - this.activeCardCenter = activeCardLeft + ((this.activeCardRight - activeCardLeft) / 2); - this.cardsGap = gap; - - this.viewUpdater = updater; - if (this.viewUpdater == null) { - this.viewUpdater = new DefaultViewUpdater(); - } - viewUpdater.onLayoutManagerInitialized(this); - } - - @Override - public RecyclerView.LayoutParams generateDefaultLayoutParams() { - return new RecyclerView.LayoutParams( - RecyclerView.LayoutParams.WRAP_CONTENT, - RecyclerView.LayoutParams.WRAP_CONTENT); - } - - @Override - public void onLayoutChildren(RecyclerView.Recycler recycler, final RecyclerView.State state) { - if (getItemCount() == 0) { - removeAndRecycleAllViews(recycler); - return; - } - - if (getChildCount() == 0 && state.isPreLayout()) { - return; - } - - int anchorPos = getActiveCardPosition(); - - if (state.isPreLayout()) { - final LinkedList removed = new LinkedList<>(); - for (int i = 0, cnt = getChildCount(); i < cnt; i++) { - final View child = getChildAt(i); - final boolean isRemoved = ((RecyclerView.LayoutParams)child.getLayoutParams()).isItemRemoved(); - if (isRemoved) { - removed.add(getPosition(child)); - } - } - - if (removed.contains(anchorPos)) { - final int first = removed.getFirst(); - final int last = removed.getLast(); - - final int left = first - 1; - final int right = last == getItemCount() + removed.size() - 1 ? RecyclerView.NO_POSITION : last; - - anchorPos = Math.max(left, right); - } - - scrollRequestedPosition = anchorPos; - } - - detachAndScrapAttachedViews(recycler); - fill(anchorPos, recycler, state); - - if (cardsXCoords.size() != 0) { - layoutByCoords(); - } - - if (state.isPreLayout()) { - recyclerView.postOnAnimationDelayed(new Runnable() { - @Override - public void run() { - updateViewScale(); - } - }, 415); - } else { - updateViewScale(); - } - } - - @Override - public boolean supportsPredictiveItemAnimations() { - return true; - } - - @Override - public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { - removeAllViews(); - } - - @Override - public boolean canScrollHorizontally() { - return getChildCount() != 0; - } - - @Override - public void scrollToPosition(int position) { - if (position < 0 || position >= getItemCount()) { - return; - } - - scrollRequestedPosition = position; - requestLayout(); - } - - @Override - public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { - scrollRequestedPosition = RecyclerView.NO_POSITION; - - int delta; - if (dx < 0) { - delta = scrollRight(Math.max(dx, -cardWidth)); - } else { - delta = scrollLeft(dx); - } - - fill(getActiveCardPosition(), recycler, state); - updateViewScale(); - - cardsXCoords.clear(); - for (int i = 0, cnt = getChildCount(); i < cnt; i++) { - final View view = getChildAt(i); - cardsXCoords.put(getPosition(view), getDecoratedLeft(view)); - } - - return delta; - } - - @Override - public PointF computeScrollVectorForPosition(int targetPosition) { - return new PointF(targetPosition - getActiveCardPosition(), 0); - } - - @Override - public void smoothScrollToPosition(final RecyclerView recyclerView, RecyclerView.State state, final int position) { - if (position < 0 || position >= getItemCount()) { - return; - } - - final LinearSmoothScroller scroller = getSmoothScroller(recyclerView); - scroller.setTargetPosition(position); - startSmoothScroll(scroller); - } - - @Override - public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int count) { - final int anchorPos = getActiveCardPosition(); - if (positionStart + count <= anchorPos) { - scrollRequestedPosition = anchorPos - 1; - } - } - - @Override - public Parcelable onSaveInstanceState() { - SavedState state = new SavedState(); - state.anchorPos = getActiveCardPosition(); - return state; - } - - @Override - public void onRestoreInstanceState(Parcelable parcelable) { - if (parcelable instanceof SavedState) { - SavedState state = (SavedState) parcelable; - scrollRequestedPosition = state.anchorPos; - requestLayout(); - } - } - - @Override - public void onAttachedToWindow(RecyclerView view) { - super.onAttachedToWindow(view); - recyclerView = view; - } - - @Override - public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { - super.onDetachedFromWindow(view, recycler); - recyclerView = null; - } - - /** - * @return active card position or RecyclerView.NO_POSITION - */ - public int getActiveCardPosition() { - if (scrollRequestedPosition != RecyclerView.NO_POSITION) { - return scrollRequestedPosition; - } else { - int result = RecyclerView.NO_POSITION; - - View biggestView = null; - float lastScaleX = 0f; - - for (int i = 0, cnt = getChildCount(); i < cnt; i++) { - final View child = getChildAt(i); - final int viewLeft = getDecoratedLeft(child); - if (viewLeft >= activeCardRight) { - continue; - } - - final float scaleX = ViewCompat.getScaleX(child); - if (lastScaleX < scaleX && viewLeft < activeCardCenter) { - lastScaleX = scaleX; - biggestView = child; - } - } - - if (biggestView != null) { - result = getPosition(biggestView); - } - - return result; - } - } - - @Nullable - public View getTopView() { - if (getChildCount() == 0) { - return null; - } - - View result = null; - float lastValue = cardWidth; - - for (int i = 0, cnt = getChildCount(); i < cnt; i++) { - final View child = getChildAt(i); - if (getDecoratedLeft(child) >= activeCardRight) { - continue; - } - - final int viewLeft = getDecoratedLeft(child); - final int diff = activeCardRight - viewLeft; - if (diff < lastValue) { - lastValue = diff; - result = child; - } - } - - return result; - } - - public int getActiveCardLeft() { - return activeCardLeft; - } - - public int getActiveCardRight() { - return activeCardRight; - } - - public int getActiveCardCenter() { - return activeCardCenter; - } - - public int getCardWidth() { - return cardWidth; - } - - public float getCardsGap() { - return cardsGap; - } - - public LinearSmoothScroller getSmoothScroller(final RecyclerView recyclerView) { - return new LinearSmoothScroller(recyclerView.getContext()) { - @Override - public int calculateDxToMakeVisible(View view, int snapPreference) { - final int viewStart = getDecoratedLeft(view); - if (viewStart > activeCardLeft) { - return activeCardLeft - viewStart; - } else { - int delta = 0; - int topViewPos = 0; - - final View topView = getTopView(); - if (topView != null) { - topViewPos = getPosition(topView); - if (topViewPos != getTargetPosition()) { - final int topViewLeft = getDecoratedLeft(topView); - if (topViewLeft >= activeCardLeft && topViewLeft < activeCardRight) { - delta = activeCardRight - topViewLeft; - } - } - } - - return delta + (cardWidth) * Math.max(0, topViewPos - getTargetPosition() - 1); - } - } - - @Override - protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { - return 0.5f; - } - - }; - } - - private ViewUpdater loadViewUpdater(Context context, String className, AttributeSet attrs) { - if (className == null || className.trim().length() == 0) { - return null; - } - - final String fullClassName; - if (className.charAt(0) == '.') { - fullClassName = context.getPackageName() + className; - } else if (className.contains(".")) { - fullClassName = className; - } else { - fullClassName = CardSliderLayoutManager.class.getPackage().getName() + '.' + className; - } - - ViewUpdater updater; - try { - final ClassLoader classLoader = context.getClassLoader(); - - final Class viewUpdaterClass = - classLoader.loadClass(fullClassName).asSubclass(ViewUpdater.class); - final Constructor constructor = - viewUpdaterClass.getConstructor(); - - constructor.setAccessible(true); - updater = constructor.newInstance(); - } catch (NoSuchMethodException e) { - throw new IllegalStateException(attrs.getPositionDescription() + - ": Error creating LayoutManager " + className, e); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(attrs.getPositionDescription() - + ": Unable to find ViewUpdater" + className, e); - } catch (InvocationTargetException e) { - throw new IllegalStateException(attrs.getPositionDescription() - + ": Could not instantiate the ViewUpdater: " + className, e); - } catch (InstantiationException e) { - throw new IllegalStateException(attrs.getPositionDescription() - + ": Could not instantiate the ViewUpdater: " + className, e); - } catch (IllegalAccessException e) { - throw new IllegalStateException(attrs.getPositionDescription() - + ": Cannot access non-public constructor " + className, e); - } catch (ClassCastException e) { - throw new IllegalStateException(attrs.getPositionDescription() - + ": Class is not a ViewUpdater " + className, e); - } - - return updater; - } - - private int scrollRight(int dx) { - final int childCount = getChildCount(); - - if (childCount == 0) { - return 0; - } - - final View rightestView = getChildAt(childCount - 1); - final int deltaBorder = activeCardLeft + getPosition(rightestView) * cardWidth; - final int delta = getAllowedRightDelta(rightestView, dx, deltaBorder); - - final LinkedList rightViews = new LinkedList<>(); - final LinkedList leftViews = new LinkedList<>(); - - for (int i = childCount - 1; i >= 0; i--) { - final View view = getChildAt(i); - final int viewLeft = getDecoratedLeft(view); - - if (viewLeft >= activeCardRight) { - rightViews.add(view); - } else { - leftViews.add(view); - } - } - - for (View view: rightViews) { - final int border = activeCardLeft + getPosition(view) * cardWidth; - final int allowedDelta = getAllowedRightDelta(view, dx, border); - view.offsetLeftAndRight(-allowedDelta); - } - - final int step = activeCardLeft / LEFT_CARD_COUNT; - final int jDelta = (int) Math.floor(1f * delta * step / cardWidth); - - View prevView = null; - int j = 0; - - for (int i = 0, cnt = leftViews.size(); i < cnt; i++) { - final View view = leftViews.get(i); - if (prevView == null || getDecoratedLeft(prevView) >= activeCardRight) { - final int border = activeCardLeft + getPosition(view) * cardWidth; - final int allowedDelta = getAllowedRightDelta(view, dx, border); - view.offsetLeftAndRight(-allowedDelta); - } else { - final int border = activeCardLeft - step * j; - view.offsetLeftAndRight(-getAllowedRightDelta(view, jDelta, border)); - j++; - } - - prevView = view; - } - - return delta; - } - - private int scrollLeft(int dx) { - final int childCount = getChildCount(); - if (childCount == 0) { - return 0; - } - - final View lastView = getChildAt(childCount - 1); - final boolean isLastItem = getPosition(lastView) == getItemCount() - 1; - - final int delta; - if (isLastItem) { - delta = Math.min(dx, getDecoratedRight(lastView) - activeCardRight); - } else { - delta = dx; - } - - final int step = activeCardLeft / LEFT_CARD_COUNT; - final int jDelta = (int) Math.ceil(1f * delta * step / cardWidth); - - for (int i = childCount - 1; i >= 0; i--) { - final View view = getChildAt(i); - final int viewLeft = getDecoratedLeft(view); - - if (viewLeft > activeCardLeft) { - view.offsetLeftAndRight(getAllowedLeftDelta(view, delta, activeCardLeft)); - } else { - int border = activeCardLeft - step; - for (int j = i; j >= 0; j--) { - final View jView = getChildAt(j); - jView.offsetLeftAndRight(getAllowedLeftDelta(jView, jDelta, border)); - border -= step; - } - - break; - } - } - - return delta; - } - - private int getAllowedLeftDelta(@NonNull View view, int dx, int border) { - final int viewLeft = getDecoratedLeft(view); - if (viewLeft - dx > border) { - return -dx; - } else { - return border - viewLeft; - } - } - - private int getAllowedRightDelta(@NonNull View view, int dx, int border) { - final int viewLeft = getDecoratedLeft(view); - if (viewLeft + Math.abs(dx) < border) { - return dx; - } else { - return viewLeft - border; - } - } - - private void layoutByCoords() { - final int count = Math.min(getChildCount(), cardsXCoords.size()); - for (int i = 0; i < count; i++) { - final View view = getChildAt(i); - final int viewLeft = cardsXCoords.get(getPosition(view)); - layoutDecorated(view, viewLeft, 0, viewLeft + cardWidth, getDecoratedBottom(view)); - } - cardsXCoords.clear(); - } - - private void fill(int anchorPos, RecyclerView.Recycler recycler, RecyclerView.State state) { - viewCache.clear(); - - for (int i = 0, cnt = getChildCount(); i < cnt; i++) { - View view = getChildAt(i); - int pos = getPosition(view); - viewCache.put(pos, view); - } - - for (int i = 0, cnt = viewCache.size(); i < cnt; i++) { - detachView(viewCache.valueAt(i)); - } - - if (!state.isPreLayout()) { - fillLeft(anchorPos, recycler); - fillRight(anchorPos, recycler); - } - - for (int i = 0, cnt = viewCache.size(); i < cnt; i++) { - recycler.recycleView(viewCache.valueAt(i)); - } - } - - private void fillLeft(int anchorPos, RecyclerView.Recycler recycler) { - if (anchorPos == RecyclerView.NO_POSITION) { - return; - } - - final int layoutStep = activeCardLeft / LEFT_CARD_COUNT; - int pos = Math.max(0, anchorPos - LEFT_CARD_COUNT - 1); - int viewLeft = Math.max(-1, LEFT_CARD_COUNT - (anchorPos - pos)) * layoutStep; - - while (pos < anchorPos) { - View view = viewCache.get(pos); - if (view != null) { - attachView(view); - viewCache.remove(pos); - } else { - view = recycler.getViewForPosition(pos); - addView(view); - measureChildWithMargins(view, 0, 0); - final int viewHeight = getDecoratedMeasuredHeight(view); - layoutDecorated(view, viewLeft, 0, viewLeft + cardWidth, viewHeight); - } - - viewLeft += layoutStep; - pos++; - } - - } - - private void fillRight(int anchorPos, RecyclerView.Recycler recycler) { - if (anchorPos == RecyclerView.NO_POSITION) { - return; - } - - final int width = getWidth(); - final int itemCount = getItemCount(); - - int pos = anchorPos; - int viewLeft = activeCardLeft; - boolean fillRight = true; - - while (fillRight && pos < itemCount) { - View view = viewCache.get(pos); - if (view != null) { - attachView(view); - viewCache.remove(pos); - } else { - view = recycler.getViewForPosition(pos); - addView(view); - measureChildWithMargins(view, 0, 0); - final int viewHeight = getDecoratedMeasuredHeight(view); - layoutDecorated(view, viewLeft, 0, viewLeft + cardWidth, viewHeight); - } - - viewLeft = getDecoratedRight(view); - fillRight = viewLeft < width + cardWidth; - pos++; - } - } - - private void updateViewScale() { - for (int i = 0, cnt = getChildCount(); i < cnt; i++) { - final View view = getChildAt(i); - final int viewLeft = getDecoratedLeft(view); - - final float position = ((float) (viewLeft - activeCardLeft) / cardWidth); - viewUpdater.updateView(view, position); - } - } - -} \ No newline at end of file diff --git a/card-slider/src/main/java/com/ramotion/cardslider/CardSliderLayoutManager.kt b/card-slider/src/main/java/com/ramotion/cardslider/CardSliderLayoutManager.kt new file mode 100644 index 0000000..73493cf --- /dev/null +++ b/card-slider/src/main/java/com/ramotion/cardslider/CardSliderLayoutManager.kt @@ -0,0 +1,620 @@ +package com.ramotion.cardslider + +import android.content.Context +import android.graphics.PointF +import android.os.Parcel +import android.os.Parcelable +import android.util.AttributeSet +import android.util.DisplayMetrics +import android.util.SparseArray +import android.util.SparseIntArray +import android.view.View +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView +import java.lang.reflect.InvocationTargetException +import java.util.* + +/** + * A [androidx.recyclerview.widget.RecyclerView.LayoutManager] implementation. + */ +class CardSliderLayoutManager : RecyclerView.LayoutManager, RecyclerView.SmoothScroller.ScrollVectorProvider { + private val viewCache = SparseArray() + private val cardsXCoords = SparseIntArray() + var cardWidth = 0 + private set + var activeCardLeft = 0 + private set + var activeCardRight = 0 + private set + var activeCardCenter = 0 + private set + var cardsGap = 0f + private set + private var scrollRequestedPosition = 0 + private var viewUpdater: ViewUpdater? = null + private var recyclerView: RecyclerView? = null + + /** + * A ViewUpdater is invoked whenever a visible/attached card is scrolled. + */ + interface ViewUpdater { + /** + * Called when CardSliderLayoutManager initialized + */ + fun onLayoutManagerInitialized(lm: CardSliderLayoutManager) + + /** + * Called on view update (scroll, layout). + * @param view Updating view + * @param position Position of card relative to the current active card position of the layout manager. + * 0 is active card. 1 is first right card, and -1 is first left (stacked) card. + */ + fun updateView(view: View, position: Float) + } + + private class SavedState : Parcelable { + var anchorPos = 0 + + internal constructor() {} + internal constructor(`in`: Parcel) { + anchorPos = `in`.readInt() + } + + constructor(other: SavedState) { + anchorPos = other.anchorPos + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(parcel: Parcel, i: Int) { + parcel.writeInt(anchorPos) + } + + companion object { + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SavedState { + return SavedState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + } + /** + * Constructor used when layout manager is set in XML by RecyclerView attribute + * "layoutManager". + * + * See [R.styleable.CardSlider_activeCardLeftOffset] + * See [R.styleable.CardSlider_cardWidth] + * See [R.styleable.CardSlider_cardsGap] + */ + /** + * Creates CardSliderLayoutManager with default values + * + * @param context Current context, will be used to access resources. + */ + @JvmOverloads + constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) { + val density = context.resources.displayMetrics.density + val defaultCardWidth = (DEFAULT_CARD_WIDTH * density).toInt() + val defaultActiveCardLeft = (DEFAULT_ACTIVE_CARD_LEFT_OFFSET * density).toInt() + val defaultCardsGap = DEFAULT_CARDS_GAP * density + if (attrs == null) { + initialize(defaultActiveCardLeft, defaultCardWidth, defaultCardsGap, null) + } else { + val attrCardWidth: Int + val attrActiveCardLeft: Int + val attrCardsGap: Float + val viewUpdateClassName: String + val a = context.theme.obtainStyledAttributes(attrs, R.styleable.CardSlider, 0, 0) + try { + attrCardWidth = a.getDimensionPixelSize(R.styleable.CardSlider_cardWidth, defaultCardWidth) + attrActiveCardLeft = a.getDimensionPixelSize(R.styleable.CardSlider_activeCardLeftOffset, defaultActiveCardLeft) + attrCardsGap = a.getDimension(R.styleable.CardSlider_cardsGap, defaultCardsGap) + viewUpdateClassName = a.getString(R.styleable.CardSlider_viewUpdater) + } finally { + a.recycle() + } + val viewUpdater = loadViewUpdater(context, viewUpdateClassName, attrs) + initialize(attrActiveCardLeft, attrCardWidth, attrCardsGap, viewUpdater) + } + } + + /** + * Creates CardSliderLayoutManager with specified values in pixels. + * + * @param activeCardLeft Active card offset from start of RecyclerView. Default value is 50dp. + * @param cardWidth Card width. Default value is 148dp. + * @param cardsGap Distance between cards. Default value is 12dp. + */ + constructor(activeCardLeft: Int, cardWidth: Int, cardsGap: Float) { + initialize(activeCardLeft, cardWidth, cardsGap, null) + } + + private fun initialize(left: Int, width: Int, gap: Float, updater: ViewUpdater?) { + cardWidth = width + activeCardLeft = left + activeCardRight = activeCardLeft + cardWidth + activeCardCenter = activeCardLeft + (activeCardRight - activeCardLeft) / 2 + cardsGap = gap + viewUpdater = updater + if (viewUpdater == null) { + viewUpdater = DefaultViewUpdater() + } + viewUpdater?.onLayoutManagerInitialized(this) + } + + override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams { + return RecyclerView.LayoutParams( + RecyclerView.LayoutParams.WRAP_CONTENT, + RecyclerView.LayoutParams.WRAP_CONTENT) + } + + override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { + if (itemCount == 0) { + removeAndRecycleAllViews(recycler) + return + } + if (childCount == 0 && state.isPreLayout) { + return + } + var anchorPos = activeCardPosition + if (state.isPreLayout) { + val removed = LinkedList() + var i = 0 + val cnt = childCount + while (i < cnt) { + val child = getChildAt(i) + val isRemoved = (child?.layoutParams as RecyclerView.LayoutParams).isItemRemoved + if (isRemoved) { + removed.add(getPosition(child)) + } + i++ + } + if (removed.contains(anchorPos)) { + val first = removed.first + val last = removed.last + val left = first - 1 + val right = if (last == itemCount + removed.size - 1) RecyclerView.NO_POSITION else last + anchorPos = Math.max(left, right) + } + scrollRequestedPosition = anchorPos + } + detachAndScrapAttachedViews(recycler) + fill(anchorPos, recycler, state) + if (cardsXCoords.size() != 0) { + layoutByCoords() + } + if (state.isPreLayout) { + recyclerView?.postOnAnimationDelayed({ updateViewScale() }, 415) + } else { + updateViewScale() + } + } + + override fun supportsPredictiveItemAnimations(): Boolean { + return true + } + + override fun onAdapterChanged(oldAdapter: RecyclerView.Adapter<*>?, newAdapter: RecyclerView.Adapter<*>?) { + removeAllViews() + } + + override fun canScrollHorizontally(): Boolean { + return childCount != 0 + } + + override fun scrollToPosition(position: Int) { + if (position < 0 || position >= itemCount) { + return + } + scrollRequestedPosition = position + requestLayout() + } + + override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int { + scrollRequestedPosition = RecyclerView.NO_POSITION + val delta: Int + delta = if (dx < 0) { + scrollRight(Math.max(dx, -cardWidth)) + } else { + scrollLeft(dx) + } + fill(activeCardPosition, recycler, state) + updateViewScale() + cardsXCoords.clear() + var i = 0 + val cnt = childCount + while (i < cnt) { + val view = getChildAt(i) + cardsXCoords.put(getPosition(view!!), getDecoratedLeft(view)) + i++ + } + return delta + } + + override fun computeScrollVectorForPosition(targetPosition: Int): PointF { + return PointF((targetPosition - activeCardPosition).toFloat(), 0f) + } + + override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State, position: Int) { + if (position < 0 || position >= itemCount) { + return + } + val scroller = getSmoothScroller(recyclerView) + scroller.targetPosition = position + startSmoothScroll(scroller) + } + + override fun onItemsRemoved(recyclerView: RecyclerView, positionStart: Int, count: Int) { + val anchorPos = activeCardPosition + if (positionStart + count <= anchorPos) { + scrollRequestedPosition = anchorPos - 1 + } + } + + override fun onSaveInstanceState(): Parcelable? { + val state = SavedState() + state.anchorPos = activeCardPosition + return state + } + + override fun onRestoreInstanceState(parcelable: Parcelable) { + if (parcelable is SavedState) { + scrollRequestedPosition = parcelable.anchorPos + requestLayout() + } + } + + override fun onAttachedToWindow(view: RecyclerView) { + super.onAttachedToWindow(view) + recyclerView = view + } + + override fun onDetachedFromWindow(view: RecyclerView, recycler: RecyclerView.Recycler) { + super.onDetachedFromWindow(view, recycler) + recyclerView = null + } + + /** + * @return active card position or RecyclerView.NO_POSITION + */ + val activeCardPosition: Int + get() = if (scrollRequestedPosition != RecyclerView.NO_POSITION) { + scrollRequestedPosition + } else { + var result = RecyclerView.NO_POSITION + var biggestView: View? = null + var lastScaleX = 0f + var i = 0 + val cnt = childCount + while (i < cnt) { + val child = getChildAt(i) + val viewLeft = getDecoratedLeft(child!!) + if (viewLeft >= activeCardRight) { + i++ + continue + } + val scaleX = ViewCompat.getScaleX(child) + if (lastScaleX < scaleX && viewLeft < activeCardCenter) { + lastScaleX = scaleX + biggestView = child + } + i++ + } + if (biggestView != null) { + result = getPosition(biggestView) + } + result + } + val topView: View? + get() { + if (childCount == 0) { + return null + } + var result: View? = null + var lastValue = cardWidth.toFloat() + var i = 0 + val cnt = childCount + while (i < cnt) { + val child = getChildAt(i) + if (getDecoratedLeft(child!!) >= activeCardRight) { + i++ + continue + } + val viewLeft = getDecoratedLeft(child) + val diff = activeCardRight - viewLeft + if (diff < lastValue) { + lastValue = diff.toFloat() + result = child + } + i++ + } + return result + } + + fun getSmoothScroller(recyclerView: RecyclerView): LinearSmoothScroller { + return object : LinearSmoothScroller(recyclerView.context) { + override fun calculateDxToMakeVisible(view: View, snapPreference: Int): Int { + val viewStart = getDecoratedLeft(view) + return if (viewStart > activeCardLeft) { + activeCardLeft - viewStart + } else { + var delta = 0 + var topViewPos = 0 + val topView = topView + if (topView != null) { + topViewPos = getPosition(topView) + if (topViewPos != targetPosition) { + val topViewLeft = getDecoratedLeft(topView) + if (topViewLeft >= activeCardLeft && topViewLeft < activeCardRight) { + delta = activeCardRight - topViewLeft + } + } + } + delta + cardWidth * Math.max(0, topViewPos - targetPosition - 1) + } + } + + override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float { + return 0.5f + } + } + } + + private fun loadViewUpdater(context: Context, className: String?, attrs: AttributeSet): ViewUpdater? { + if (className == null || className.trim { it <= ' ' }.length == 0) { + return null + } + val fullClassName: String + fullClassName = if (className[0] == '.') { + context.packageName + className + } else if (className.contains(".")) { + className + } else { + CardSliderLayoutManager::class.java.getPackage().name + '.' + className + } + val updater: ViewUpdater + try { + val classLoader = context.classLoader + val viewUpdaterClass = classLoader.loadClass(fullClassName).asSubclass(ViewUpdater::class.java) + val constructor = viewUpdaterClass.getConstructor() + constructor.isAccessible = true + updater = constructor.newInstance() + } catch (e: NoSuchMethodException) { + throw IllegalStateException(attrs.positionDescription + + ": Error creating LayoutManager " + className, e) + } catch (e: ClassNotFoundException) { + throw IllegalStateException(attrs.positionDescription + + ": Unable to find ViewUpdater" + className, e) + } catch (e: InvocationTargetException) { + throw IllegalStateException(attrs.positionDescription + + ": Could not instantiate the ViewUpdater: " + className, e) + } catch (e: InstantiationException) { + throw IllegalStateException(attrs.positionDescription + + ": Could not instantiate the ViewUpdater: " + className, e) + } catch (e: IllegalAccessException) { + throw IllegalStateException(attrs.positionDescription + + ": Cannot access non-public constructor " + className, e) + } catch (e: ClassCastException) { + throw IllegalStateException(attrs.positionDescription + + ": Class is not a ViewUpdater " + className, e) + } + return updater + } + + private fun scrollRight(dx: Int): Int { + val childCount = childCount + if (childCount == 0) { + return 0 + } + val rightestView = getChildAt(childCount - 1) + val deltaBorder = activeCardLeft + getPosition(rightestView!!) * cardWidth + val delta = getAllowedRightDelta(rightestView, dx, deltaBorder) + val rightViews = LinkedList() + val leftViews = LinkedList() + for (i in childCount - 1 downTo 0) { + val view = getChildAt(i) + val viewLeft = getDecoratedLeft(view!!) + if (viewLeft >= activeCardRight) { + rightViews.add(view) + } else { + leftViews.add(view) + } + } + for (view in rightViews) { + val border = activeCardLeft + getPosition(view!!) * cardWidth + val allowedDelta = getAllowedRightDelta(view, dx, border) + view.offsetLeftAndRight(-allowedDelta) + } + val step = activeCardLeft / LEFT_CARD_COUNT + val jDelta = Math.floor((1f * delta * step / cardWidth).toDouble()).toInt() + var prevView: View? = null + var j = 0 + var i = 0 + val cnt = leftViews.size + while (i < cnt) { + val view = leftViews[i]!! + if (prevView == null || getDecoratedLeft(prevView) >= activeCardRight) { + val border = activeCardLeft + getPosition(view) * cardWidth + val allowedDelta = getAllowedRightDelta(view, dx, border) + view.offsetLeftAndRight(-allowedDelta) + } else { + val border = activeCardLeft - step * j + view.offsetLeftAndRight(-getAllowedRightDelta(view, jDelta, border)) + j++ + } + prevView = view + i++ + } + return delta + } + + private fun scrollLeft(dx: Int): Int { + val childCount = childCount + if (childCount == 0) { + return 0 + } + val lastView = getChildAt(childCount - 1) + val isLastItem = getPosition(lastView!!) == itemCount - 1 + val delta: Int + delta = if (isLastItem) { + Math.min(dx, getDecoratedRight(lastView) - activeCardRight) + } else { + dx + } + val step = activeCardLeft / LEFT_CARD_COUNT + val jDelta = Math.ceil((1f * delta * step / cardWidth).toDouble()).toInt() + for (i in childCount - 1 downTo 0) { + val view = getChildAt(i) + val viewLeft = getDecoratedLeft(view!!) + if (viewLeft > activeCardLeft) { + view.offsetLeftAndRight(getAllowedLeftDelta(view, delta, activeCardLeft)) + } else { + var border = activeCardLeft - step + for (j in i downTo 0) { + val jView = getChildAt(j) + jView?.offsetLeftAndRight(getAllowedLeftDelta(jView, jDelta, border)) + border -= step + } + break + } + } + return delta + } + + private fun getAllowedLeftDelta(view: View, dx: Int, border: Int): Int { + val viewLeft = getDecoratedLeft(view) + return if (viewLeft - dx > border) { + -dx + } else { + border - viewLeft + } + } + + private fun getAllowedRightDelta(view: View, dx: Int, border: Int): Int { + val viewLeft = getDecoratedLeft(view) + return if (viewLeft + Math.abs(dx) < border) { + dx + } else { + viewLeft - border + } + } + + private fun layoutByCoords() { + val count = Math.min(childCount, cardsXCoords.size()) + for (i in 0 until count) { + val view = getChildAt(i) + val viewLeft = cardsXCoords[getPosition(view!!)] + layoutDecorated(view, viewLeft, 0, viewLeft + cardWidth, getDecoratedBottom(view)) + } + cardsXCoords.clear() + } + + private fun fill(anchorPos: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State) { + viewCache.clear() + run { + var i = 0 + val cnt = childCount + while (i < cnt) { + val view = getChildAt(i) + val pos = getPosition(view!!) + viewCache.put(pos, view) + i++ + } + } + run { + var i = 0 + val cnt = viewCache.size() + while (i < cnt) { + detachView(viewCache.valueAt(i)!!) + i++ + } + } + if (!state.isPreLayout) { + fillLeft(anchorPos, recycler) + fillRight(anchorPos, recycler) + } + var i = 0 + val cnt = viewCache.size() + while (i < cnt) { + recycler.recycleView(viewCache.valueAt(i)!!) + i++ + } + } + + private fun fillLeft(anchorPos: Int, recycler: RecyclerView.Recycler) { + if (anchorPos == RecyclerView.NO_POSITION) { + return + } + val layoutStep = activeCardLeft / LEFT_CARD_COUNT + var pos = Math.max(0, anchorPos - LEFT_CARD_COUNT - 1) + var viewLeft = Math.max(-1, LEFT_CARD_COUNT - (anchorPos - pos)) * layoutStep + while (pos < anchorPos) { + var view = viewCache[pos] + if (view != null) { + attachView(view) + viewCache.remove(pos) + } else { + view = recycler.getViewForPosition(pos) + addView(view) + measureChildWithMargins(view, 0, 0) + val viewHeight = getDecoratedMeasuredHeight(view) + layoutDecorated(view, viewLeft, 0, viewLeft + cardWidth, viewHeight) + } + viewLeft += layoutStep + pos++ + } + } + + private fun fillRight(anchorPos: Int, recycler: RecyclerView.Recycler) { + if (anchorPos == RecyclerView.NO_POSITION) { + return + } + val width = width + val itemCount = itemCount + var pos = anchorPos + var viewLeft = activeCardLeft + var fillRight = true + while (fillRight && pos < itemCount) { + var view = viewCache[pos] + if (view != null) { + attachView(view) + viewCache.remove(pos) + } else { + view = recycler.getViewForPosition(pos) + addView(view) + measureChildWithMargins(view, 0, 0) + val viewHeight = getDecoratedMeasuredHeight(view) + layoutDecorated(view, viewLeft, 0, viewLeft + cardWidth, viewHeight) + } + viewLeft = getDecoratedRight(view) + fillRight = viewLeft < width + cardWidth + pos++ + } + } + + private fun updateViewScale() { + var i = 0 + val cnt = childCount + while (i < cnt) { + val view = getChildAt(i) + val viewLeft = getDecoratedLeft(view!!) + val position = (viewLeft - activeCardLeft).toFloat() / cardWidth + viewUpdater?.updateView(view, position) + i++ + } + } + + companion object { + private const val DEFAULT_ACTIVE_CARD_LEFT_OFFSET = 50 + private const val DEFAULT_CARD_WIDTH = 148 + private const val DEFAULT_CARDS_GAP = 12 + private const val LEFT_CARD_COUNT = 2 + } +} \ No newline at end of file diff --git a/card-slider/src/main/java/com/ramotion/cardslider/CardSnapHelper.java b/card-slider/src/main/java/com/ramotion/cardslider/CardSnapHelper.java deleted file mode 100644 index e631c62..0000000 --- a/card-slider/src/main/java/com/ramotion/cardslider/CardSnapHelper.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.ramotion.cardslider; - -import android.graphics.PointF; -import android.view.View; -import android.view.animation.AccelerateInterpolator; - -import java.security.InvalidParameterException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearSmoothScroller; -import androidx.recyclerview.widget.LinearSnapHelper; -import androidx.recyclerview.widget.RecyclerView; - -/** - * Extended {@link LinearSnapHelper} that works only with {@link CardSliderLayoutManager}. - */ -public class CardSnapHelper extends LinearSnapHelper { - - private RecyclerView recyclerView; - - /** - * Attaches the {@link CardSnapHelper} 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 SnapHelper from the current - * RecyclerView. - * - * @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener} - * attached to the provided {@link RecyclerView}. - * - * @throws InvalidParameterException if provided RecyclerView has LayoutManager which is not - * instance of CardSliderLayoutManager - * - */ - @Override - public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException { - super.attachToRecyclerView(recyclerView); - - if (recyclerView != null && !(recyclerView.getLayoutManager() instanceof CardSliderLayoutManager)) { - throw new InvalidParameterException("LayoutManager must be instance of CardSliderLayoutManager"); - } - - this.recyclerView = recyclerView; - } - - @Override - public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) { - final CardSliderLayoutManager lm = (CardSliderLayoutManager) layoutManager; - - final int itemCount = lm.getItemCount(); - if (itemCount == 0) { - return RecyclerView.NO_POSITION; - } - - final RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider = - (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager; - - final PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1); - if (vectorForEnd == null) { - return RecyclerView.NO_POSITION; - } - - final int distance = calculateScrollDistance(velocityX, velocityY)[0]; - int deltaJump; - - if (distance > 0) { - deltaJump = (int) Math.floor(distance / lm.getCardWidth()); - } else { - deltaJump = (int) Math.ceil(distance / lm.getCardWidth()); - } - - final int deltaSign = Integer.signum(deltaJump); - deltaJump = deltaSign * Math.min(3, Math.abs(deltaJump)); - - if (vectorForEnd.x < 0) { - deltaJump = -deltaJump; - } - - if (deltaJump == 0) { - return RecyclerView.NO_POSITION; - } - - final int currentPosition = lm.getActiveCardPosition(); - if (currentPosition == RecyclerView.NO_POSITION) { - return RecyclerView.NO_POSITION; - } - - int targetPos = currentPosition + deltaJump; - if (targetPos < 0 || targetPos >= itemCount) { - targetPos = RecyclerView.NO_POSITION; - } - - return targetPos; - } - - @Override - public View findSnapView(RecyclerView.LayoutManager layoutManager) { - return ((CardSliderLayoutManager)layoutManager).getTopView(); - } - - @Override - public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, - @NonNull View targetView) - { - final CardSliderLayoutManager lm = (CardSliderLayoutManager)layoutManager; - final int viewLeft = lm.getDecoratedLeft(targetView); - final int activeCardLeft = lm.getActiveCardLeft(); - final int activeCardCenter = lm.getActiveCardLeft() + lm.getCardWidth() / 2; - final int activeCardRight = lm.getActiveCardLeft() + lm.getCardWidth(); - - int[] out = new int[] {0, 0}; - if (viewLeft < activeCardCenter) { - final int targetPos = lm.getPosition(targetView); - final int activeCardPos = lm.getActiveCardPosition(); - if (targetPos != activeCardPos) { - out[0] = -(activeCardPos - targetPos) * lm.getCardWidth(); - } else { - out[0] = viewLeft - activeCardLeft; - } - } else { - out[0] = viewLeft - activeCardRight + 1; - } - - if (out[0] != 0) { - recyclerView.smoothScrollBy(out[0], 0, new AccelerateInterpolator()); - } - - return new int[] {0, 0}; - } - - @Nullable - @Override - protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) { - return ((CardSliderLayoutManager)layoutManager).getSmoothScroller(recyclerView); - } - -} diff --git a/card-slider/src/main/java/com/ramotion/cardslider/CardSnapHelper.kt b/card-slider/src/main/java/com/ramotion/cardslider/CardSnapHelper.kt new file mode 100644 index 0000000..565c3ff --- /dev/null +++ b/card-slider/src/main/java/com/ramotion/cardslider/CardSnapHelper.kt @@ -0,0 +1,108 @@ +package com.ramotion.cardslider + +import android.view.View +import android.view.animation.AccelerateInterpolator +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.LinearSnapHelper +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider +import java.security.InvalidParameterException + +/** + * Extended [LinearSnapHelper] that works **only** with [CardSliderLayoutManager]. + */ +class CardSnapHelper : LinearSnapHelper() { + private var recyclerView: RecyclerView? = null + + /** + * Attaches the [CardSnapHelper] to the provided RecyclerView, by calling + * [RecyclerView.setOnFlingListener]. + * You can call this method with `null` to detach it from the current RecyclerView. + * + * @param recyclerView The RecyclerView instance to which you want to add this helper or + * `null` if you want to remove SnapHelper from the current + * RecyclerView. + * + * @throws IllegalArgumentException if there is already a [RecyclerView.OnFlingListener] + * attached to the provided [RecyclerView]. + * + * @throws InvalidParameterException if provided RecyclerView has LayoutManager which is not + * instance of CardSliderLayoutManager + */ + @Throws(IllegalStateException::class) + override fun attachToRecyclerView(recyclerView: RecyclerView?) { + super.attachToRecyclerView(recyclerView) + if (recyclerView != null && recyclerView.layoutManager !is CardSliderLayoutManager) { + throw InvalidParameterException("LayoutManager must be instance of CardSliderLayoutManager") + } + this.recyclerView = recyclerView + } + + override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int { + val lm = layoutManager as CardSliderLayoutManager + val itemCount = lm.itemCount + if (itemCount == 0) { + return RecyclerView.NO_POSITION + } + val vectorProvider = layoutManager as ScrollVectorProvider + val vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1) + ?: return RecyclerView.NO_POSITION + val distance = calculateScrollDistance(velocityX, velocityY)[0] + var deltaJump: Int + deltaJump = if (distance > 0) { + Math.floor((distance / lm.cardWidth).toDouble()).toInt() + } else { + Math.ceil((distance / lm.cardWidth).toDouble()).toInt() + } + val deltaSign = Integer.signum(deltaJump) + deltaJump = deltaSign * Math.min(3, Math.abs(deltaJump)) + if (vectorForEnd.x < 0) { + deltaJump = -deltaJump + } + if (deltaJump == 0) { + return RecyclerView.NO_POSITION + } + val currentPosition = lm.activeCardPosition + if (currentPosition == RecyclerView.NO_POSITION) { + return RecyclerView.NO_POSITION + } + var targetPos = currentPosition + deltaJump + if (targetPos < 0 || targetPos >= itemCount) { + targetPos = RecyclerView.NO_POSITION + } + return targetPos + } + + override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? { + return (layoutManager as CardSliderLayoutManager).topView + } + + override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, + targetView: View): IntArray? { + val lm = layoutManager as CardSliderLayoutManager + val viewLeft = lm.getDecoratedLeft(targetView) + val activeCardLeft = lm.activeCardLeft + val activeCardCenter = lm.activeCardLeft + lm.cardWidth / 2 + val activeCardRight = lm.activeCardLeft + lm.cardWidth + val out = intArrayOf(0, 0) + if (viewLeft < activeCardCenter) { + val targetPos = lm.getPosition(targetView) + val activeCardPos = lm.activeCardPosition + if (targetPos != activeCardPos) { + out[0] = -(activeCardPos - targetPos) * lm.cardWidth + } else { + out[0] = viewLeft - activeCardLeft + } + } else { + out[0] = viewLeft - activeCardRight + 1 + } + if (out[0] != 0) { + recyclerView?.smoothScrollBy(out[0], 0, AccelerateInterpolator()) + } + return intArrayOf(0, 0) + } + + override fun createSnapScroller(layoutManager: RecyclerView.LayoutManager): LinearSmoothScroller? { + return recyclerView?.let { (layoutManager as CardSliderLayoutManager).getSmoothScroller(it) } + } +} diff --git a/card-slider/src/main/java/com/ramotion/cardslider/DefaultViewUpdater.java b/card-slider/src/main/java/com/ramotion/cardslider/DefaultViewUpdater.java deleted file mode 100644 index 0f4ecdc..0000000 --- a/card-slider/src/main/java/com/ramotion/cardslider/DefaultViewUpdater.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.ramotion.cardslider; - -import androidx.annotation.NonNull; -import androidx.core.view.ViewCompat; -import android.view.View; - -/** - * Default implementation of {@link CardSliderLayoutManager.ViewUpdater} - */ -public class DefaultViewUpdater implements CardSliderLayoutManager.ViewUpdater { - - public static final float SCALE_LEFT = 0.65f; - public static final float SCALE_CENTER = 0.95f; - public static final float SCALE_RIGHT = 0.8f; - public static final float SCALE_CENTER_TO_LEFT = SCALE_CENTER - SCALE_LEFT; - public static final float SCALE_CENTER_TO_RIGHT = SCALE_CENTER - SCALE_RIGHT; - - public static final int Z_CENTER_1 = 12; - public static final int Z_CENTER_2 = 16; - public static final int Z_RIGHT = 8; - - private int cardWidth; - private int activeCardLeft; - private int activeCardRight; - private int activeCardCenter; - private float cardsGap; - - private int transitionEnd; - private int transitionDistance; - private float transitionRight2Center; - - private CardSliderLayoutManager lm; - - private View previewView; - - @Override - public void onLayoutManagerInitialized(@NonNull CardSliderLayoutManager lm) { - this.lm = lm; - - this.cardWidth = lm.getCardWidth(); - this.activeCardLeft = lm.getActiveCardLeft(); - this.activeCardRight = lm.getActiveCardRight(); - this.activeCardCenter = lm.getActiveCardCenter(); - this.cardsGap = lm.getCardsGap(); - - this.transitionEnd = activeCardCenter; - this.transitionDistance = activeCardRight - transitionEnd; - - final float centerBorder = (cardWidth - cardWidth * SCALE_CENTER) / 2f; - final float rightBorder = (cardWidth - cardWidth * SCALE_RIGHT) / 2f; - final float right2centerDistance = (activeCardRight + centerBorder) - (activeCardRight - rightBorder); - this.transitionRight2Center = right2centerDistance - cardsGap; - } - - @Override - public void updateView(@NonNull View view, float position) { - final float scale; - final float alpha; - final float z; - final float x; - - if (position < 0) { - final float ratio = (float) lm.getDecoratedLeft(view) / activeCardLeft; - scale = SCALE_LEFT + SCALE_CENTER_TO_LEFT * ratio; - alpha = 0.1f + ratio; - z = Z_CENTER_1 * ratio; - x = 0; - } else if (position < 0.5f) { - scale = SCALE_CENTER; - alpha = 1; - z = Z_CENTER_1; - x = 0; - } else if (position < 1f) { - final int viewLeft = lm.getDecoratedLeft(view); - final float ratio = (float) (viewLeft - activeCardCenter) / (activeCardRight - activeCardCenter); - scale = SCALE_CENTER - SCALE_CENTER_TO_RIGHT * ratio; - alpha = 1; - z = Z_CENTER_2; - if (Math.abs(transitionRight2Center) < Math.abs(transitionRight2Center * (viewLeft - transitionEnd) / transitionDistance)) { - x = -transitionRight2Center; - } else { - x = -transitionRight2Center * (viewLeft - transitionEnd) / transitionDistance; - } - } else { - scale = SCALE_RIGHT; - alpha = 1; - z = Z_RIGHT; - - if (previewView != null) { - final float prevViewScale; - final float prevTransition; - final int prevRight; - - final boolean isFirstRight = lm.getDecoratedRight(previewView) <= activeCardRight; - if (isFirstRight) { - prevViewScale = SCALE_CENTER; - prevRight = activeCardRight; - prevTransition = 0; - } else { - prevViewScale = ViewCompat.getScaleX(previewView); - prevRight = lm.getDecoratedRight(previewView); - prevTransition = ViewCompat.getTranslationX(previewView); - } - - final float prevBorder = (cardWidth - cardWidth * prevViewScale) / 2; - final float currentBorder = (cardWidth - cardWidth * SCALE_RIGHT) / 2; - final float distance = (lm.getDecoratedLeft(view) + currentBorder) - (prevRight - prevBorder + prevTransition); - - final float transition = distance - cardsGap; - x = -transition; - } else { - x = 0; - } - } - - ViewCompat.setScaleX(view, scale); - ViewCompat.setScaleY(view, scale); - ViewCompat.setZ(view, z); - ViewCompat.setTranslationX(view, x); - ViewCompat.setAlpha(view, alpha); - - previewView = view; - } - - protected CardSliderLayoutManager getLayoutManager() { - return lm; - } - -} diff --git a/card-slider/src/main/java/com/ramotion/cardslider/DefaultViewUpdater.kt b/card-slider/src/main/java/com/ramotion/cardslider/DefaultViewUpdater.kt new file mode 100644 index 0000000..02d3be1 --- /dev/null +++ b/card-slider/src/main/java/com/ramotion/cardslider/DefaultViewUpdater.kt @@ -0,0 +1,109 @@ +package com.ramotion.cardslider + +import android.view.View +import androidx.core.view.ViewCompat +import com.ramotion.cardslider.CardSliderLayoutManager.ViewUpdater + +/** + * Default implementation of [CardSliderLayoutManager.ViewUpdater] + */ +open class DefaultViewUpdater : ViewUpdater { + private var cardWidth = 0 + private var activeCardLeft = 0 + private var activeCardRight = 0 + private var activeCardCenter = 0 + private var cardsGap = 0f + private var transitionEnd = 0 + private var transitionDistance = 0 + private var transitionRight2Center = 0f + protected var layoutManager: CardSliderLayoutManager? = null + private set + private var previewView: View? = null + override fun onLayoutManagerInitialized(lm: CardSliderLayoutManager) { + layoutManager = lm + cardWidth = lm.cardWidth + activeCardLeft = lm.activeCardLeft + activeCardRight = lm.activeCardRight + activeCardCenter = lm.activeCardCenter + cardsGap = lm.cardsGap + transitionEnd = activeCardCenter + transitionDistance = activeCardRight - transitionEnd + val centerBorder = (cardWidth - cardWidth * SCALE_CENTER) / 2f + val rightBorder = (cardWidth - cardWidth * SCALE_RIGHT) / 2f + val right2centerDistance = activeCardRight + centerBorder - (activeCardRight - rightBorder) + transitionRight2Center = right2centerDistance - cardsGap + } + + override fun updateView(view: View, position: Float) { + val scale: Float + val alpha: Float + val z: Float + val x: Float + if (position < 0) { + val ratio = (layoutManager?.getDecoratedLeft(view)?.toFloat() ?: 0f) / activeCardLeft + scale = SCALE_LEFT + SCALE_CENTER_TO_LEFT * ratio + alpha = 0.1f + ratio + z = Z_CENTER_1 * ratio + x = 0f + } else if (position < 0.5f) { + scale = SCALE_CENTER + alpha = 1f + z = Z_CENTER_1.toFloat() + x = 0f + } else if (position < 1f) { + val viewLeft = layoutManager?.getDecoratedLeft(view) ?: 0 + val ratio = (viewLeft - activeCardCenter).toFloat() / (activeCardRight - activeCardCenter) + scale = SCALE_CENTER - SCALE_CENTER_TO_RIGHT * ratio + alpha = 1f + z = Z_CENTER_2.toFloat() + x = if (Math.abs(transitionRight2Center) < Math.abs(transitionRight2Center * (viewLeft - transitionEnd) / transitionDistance)) { + -transitionRight2Center + } else { + -transitionRight2Center * (viewLeft - transitionEnd) / transitionDistance + } + } else { + scale = SCALE_RIGHT + alpha = 1f + z = Z_RIGHT.toFloat() + if (previewView != null && layoutManager != null) { + val prevViewScale: Float + val prevTransition: Float + val prevRight: Int + val isFirstRight = layoutManager!!.getDecoratedRight(previewView!!) <= activeCardRight + if (isFirstRight) { + prevViewScale = SCALE_CENTER + prevRight = activeCardRight + prevTransition = 0f + } else { + prevViewScale = previewView?.scaleX ?: 0f + prevRight = layoutManager!!.getDecoratedRight(previewView!!) + prevTransition = previewView?.translationX ?: 0f + } + val prevBorder = (cardWidth - cardWidth * prevViewScale) / 2 + val currentBorder = (cardWidth - cardWidth * SCALE_RIGHT) / 2 + val distance = layoutManager!!.getDecoratedLeft(view) + currentBorder - (prevRight - prevBorder + prevTransition) + val transition = distance - cardsGap + x = -transition + } else { + x = 0f + } + } + view.scaleX = scale + view.scaleY = scale + ViewCompat.setZ(view, z) + view.translationX = x + view.alpha = alpha + previewView = view + } + + companion object { + const val SCALE_LEFT = 0.65f + const val SCALE_CENTER = 0.95f + const val SCALE_RIGHT = 0.8f + const val SCALE_CENTER_TO_LEFT = SCALE_CENTER - SCALE_LEFT + const val SCALE_CENTER_TO_RIGHT = SCALE_CENTER - SCALE_RIGHT + const val Z_CENTER_1 = 12 + const val Z_CENTER_2 = 16 + const val Z_RIGHT = 8 + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5545196..92145f9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip