diff --git a/app/build.gradle b/app/build.gradle
index 1c0e75aa78..ad5fcc5f39 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -20,12 +20,12 @@ android {
}
multiDexEnabled true
- externalNativeBuild {
- ndk {
- // 设置支持的SO库架构
- abiFilters 'armeabi-v7a'
- }
- }
+// externalNativeBuild {
+// ndk {
+// // 设置支持的SO库架构
+// abiFilters 'armeabi-v7a'
+// }
+// }
}
signingConfigs {
release {
diff --git a/build.gradle b/build.gradle
index fe76aac9ed..0b6d343d97 100644
--- a/build.gradle
+++ b/build.gradle
@@ -21,6 +21,8 @@ buildscript {
classpath 'com.android.tools.build:gradle:3.5.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.alibaba:arouter-register:1.0.2"
+ classpath 'com.novoda:bintray-release:0.8.0'
+
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
diff --git a/config.gradle b/config.gradle
index 5cdac9cd7e..996479e8ca 100644
--- a/config.gradle
+++ b/config.gradle
@@ -89,7 +89,8 @@ ext {
modulecommon : "com.mogo.module:module-common:${MOGO_MODULE_COMMON_VERSION}",
modulemain : "com.mogo.module:module-main:${MOGO_MODULE_MAIN_VERSION}",
modulemap : "com.mogo.module:module-map:${MOGO_MODULE_MAP_VERSION}",
- moduleservice : "com.mogo.module:module-service:${MOGO_MODULE_SERVICE_VERSION}",
+ moduleservice : "com.mogo.module:module-service:${CARD_LIBRARY_VERSION}",
+ uicard : "com.mogo.ui:card-libaray:${MOGO_MODULE_SERVICE_VERSION}",
mogoservice : "com.mogo.service:mogo-service:${MOGO_SERVICE_VERSION}",
mogoserviceapi : "com.mogo.service:mogo-service-api:${MOGO_SERVICE_API_VERSION}",
moduleapps : "com.mogo.module:module-apps:${MOGO_MODULE_APPS_VERSION}",
diff --git a/gradle.properties b/gradle.properties
index d4e995741d..e0824405ce 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -66,3 +66,5 @@ MOGO_MODULE_AD_CARD_VERSION=1.0.0-SNAPSHOT
# 新鲜水
MOGO_MODULE_FRESH_NEWS_VERSION=1.0.0-SNAPSHOT
+# 卡片效果
+CARD_LIBRARY_VERSION=1.0.0-SNAPSHOT
\ No newline at end of file
diff --git a/libraries/card-library/.gitignore b/libraries/card-library/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/libraries/card-library/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/libraries/card-library/build.gradle b/libraries/card-library/build.gradle
new file mode 100644
index 0000000000..de2e3eddb9
--- /dev/null
+++ b/libraries/card-library/build.gradle
@@ -0,0 +1,36 @@
+apply plugin: 'com.android.library'
+apply plugin: 'com.novoda.bintray-release'
+
+android {
+ compileSdkVersion rootProject.ext.android.compileSdkVersion
+
+ defaultConfig {
+ targetSdkVersion rootProject.ext.android.targetSdkVersion
+ minSdkVersion rootProject.ext.android.minSdkVersion
+ versionCode Integer.valueOf(VERSION_CODE)
+ versionName getValueFromRootProperties("${project.name.replace("-", "_").toUpperCase()}_VERSION")
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+}
+
+dependencies {
+ implementation rootProject.ext.dependencies.androidxappcompat
+ implementation rootProject.ext.dependencies.androidxrecyclerview
+ testImplementation 'org.robolectric:robolectric:3.0'
+ testImplementation 'junit:junit:4.12'
+ testImplementation 'org.mockito:mockito-core:2.13.0'
+ testImplementation 'org.hamcrest:hamcrest-library:1.3'
+ androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'
+}
+apply from: new File(rootProject.rootDir, "gradle/upload.gradle").toString()
+
+//publish {
+// artifactId = 'discrete-scrollview'
+// userOrg = rootProject.userOrg
+// groupId = rootProject.groupId
+// uploadName = rootProject.uploadName
+// publishVersion = rootProject.publishVersion
+// description = rootProject.description
+// licences = rootProject.licences
+//}
\ No newline at end of file
diff --git a/libraries/card-library/gradle.properties b/libraries/card-library/gradle.properties
new file mode 100644
index 0000000000..1d49d2e63f
--- /dev/null
+++ b/libraries/card-library/gradle.properties
@@ -0,0 +1,3 @@
+GROUP=com.mogo.ui
+POM_ARTIFACT_ID=card-libaray
+VERSION_CODE=1
\ No newline at end of file
diff --git a/libraries/card-library/proguard-rules.pro b/libraries/card-library/proguard-rules.pro
new file mode 100644
index 0000000000..2309852b8f
--- /dev/null
+++ b/libraries/card-library/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in C:\Users\yarolegovich\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/libraries/card-library/src/main/AndroidManifest.xml b/libraries/card-library/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..f4432f4932
--- /dev/null
+++ b/libraries/card-library/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java
new file mode 100644
index 0000000000..d00779e027
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java
@@ -0,0 +1,219 @@
+package com.yarolegovich.discretescrollview;
+
+import android.graphics.Point;
+import android.view.View;
+
+/**
+ * Created by yarolegovich on 16.03.2017.
+ */
+public enum DSVOrientation {
+
+ HORIZONTAL {
+ @Override
+ Helper createHelper() {
+ return new HorizontalHelper();
+ }
+ },
+ VERTICAL {
+ @Override
+ Helper createHelper() {
+ return new VerticalHelper();
+ }
+ };
+
+ //Package private
+ abstract Helper createHelper();
+
+ interface Helper {
+
+ int getViewEnd(int recyclerWidth, int recyclerHeight);
+
+ int getDistanceToChangeCurrent(int childWidth, int childHeight);
+
+ void setCurrentViewCenter(Point recyclerCenter, int scrolled, Point outPoint);
+
+ void shiftViewCenter(Direction direction, int shiftAmount, Point outCenter);
+
+ int getFlingVelocity(int velocityX, int velocityY);
+
+ int getPendingDx(int pendingScroll);
+
+ int getPendingDy(int pendingScroll);
+
+ void offsetChildren(int amount, RecyclerViewProxy lm);
+
+ float getDistanceFromCenter(Point center, int viewCenterX, int viewCenterY);
+
+ boolean isViewVisible(Point center, int halfWidth, int halfHeight, int endBound, int extraSpace);
+
+ boolean hasNewBecomeVisible(DiscreteScrollLayoutManager lm);
+
+ boolean canScrollVertically();
+
+ boolean canScrollHorizontally();
+ }
+
+ protected static class HorizontalHelper implements Helper {
+
+ @Override
+ public int getViewEnd(int recyclerWidth, int recyclerHeight) {
+ return recyclerWidth;
+ }
+
+ @Override
+ public int getDistanceToChangeCurrent(int childWidth, int childHeight) {
+ return childWidth;
+ }
+
+ @Override
+ public void setCurrentViewCenter(Point recyclerCenter, int scrolled, Point outPoint) {
+ int newX = recyclerCenter.x - scrolled;
+ outPoint.set(newX, recyclerCenter.y);
+ }
+
+ @Override
+ public void shiftViewCenter(Direction direction, int shiftAmount, Point outCenter) {
+ int newX = outCenter.x + direction.applyTo(shiftAmount);
+ outCenter.set(newX, outCenter.y);
+ }
+
+ @Override
+ public boolean isViewVisible(
+ Point viewCenter, int halfWidth, int halfHeight, int endBound,
+ int extraSpace) {
+ int viewLeft = viewCenter.x - halfWidth;
+ int viewRight = viewCenter.x + halfWidth;
+ return viewLeft < (endBound + extraSpace) && viewRight > -extraSpace;
+ }
+
+ @Override
+ public boolean hasNewBecomeVisible(DiscreteScrollLayoutManager lm) {
+ View firstChild = lm.getFirstChild(), lastChild = lm.getLastChild();
+ int leftBound = -lm.getExtraLayoutSpace();
+ int rightBound = lm.getWidth() + lm.getExtraLayoutSpace();
+ boolean isNewVisibleFromLeft = lm.getDecoratedLeft(firstChild) > leftBound
+ && lm.getPosition(firstChild) > 0;
+ boolean isNewVisibleFromRight = lm.getDecoratedRight(lastChild) < rightBound
+ && lm.getPosition(lastChild) < lm.getItemCount() - 1;
+ return isNewVisibleFromLeft || isNewVisibleFromRight;
+ }
+
+ @Override
+ public void offsetChildren(int amount, RecyclerViewProxy helper) {
+ helper.offsetChildrenHorizontal(amount);
+ }
+
+ @Override
+ public float getDistanceFromCenter(Point center, int viewCenterX, int viewCenterY) {
+ return viewCenterX - center.x;
+ }
+
+ @Override
+ public int getFlingVelocity(int velocityX, int velocityY) {
+ return velocityX;
+ }
+
+ @Override
+ public boolean canScrollHorizontally() {
+ return true;
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return false;
+ }
+
+ @Override
+ public int getPendingDx(int pendingScroll) {
+ return pendingScroll;
+ }
+
+ @Override
+ public int getPendingDy(int pendingScroll) {
+ return 0;
+ }
+ }
+
+
+ protected static class VerticalHelper implements Helper {
+
+ @Override
+ public int getViewEnd(int recyclerWidth, int recyclerHeight) {
+ return recyclerHeight;
+ }
+
+ @Override
+ public int getDistanceToChangeCurrent(int childWidth, int childHeight) {
+ return childHeight;
+ }
+
+ @Override
+ public void setCurrentViewCenter(Point recyclerCenter, int scrolled, Point outPoint) {
+ int newY = recyclerCenter.y - scrolled;
+ outPoint.set(recyclerCenter.x, newY);
+ }
+
+ @Override
+ public void shiftViewCenter(Direction direction, int shiftAmount, Point outCenter) {
+ int newY = outCenter.y + direction.applyTo(shiftAmount);
+ outCenter.set(outCenter.x, newY);
+ }
+
+ @Override
+ public void offsetChildren(int amount, RecyclerViewProxy helper) {
+ helper.offsetChildrenVertical(amount);
+ }
+
+ @Override
+ public float getDistanceFromCenter(Point center, int viewCenterX, int viewCenterY) {
+ return viewCenterY - center.y;
+ }
+
+ @Override
+ public boolean isViewVisible(
+ Point viewCenter, int halfWidth, int halfHeight, int endBound,
+ int extraSpace) {
+ int viewTop = viewCenter.y - halfHeight;
+ int viewBottom = viewCenter.y + halfHeight;
+ return viewTop < (endBound + extraSpace) && viewBottom > -extraSpace;
+ }
+
+ @Override
+ public boolean hasNewBecomeVisible(DiscreteScrollLayoutManager lm) {
+ View firstChild = lm.getFirstChild(), lastChild = lm.getLastChild();
+ int topBound = -lm.getExtraLayoutSpace();
+ int bottomBound = lm.getHeight() + lm.getExtraLayoutSpace();
+ boolean isNewVisibleFromTop = lm.getDecoratedTop(firstChild) > topBound
+ && lm.getPosition(firstChild) > 0;
+ boolean isNewVisibleFromBottom = lm.getDecoratedBottom(lastChild) < bottomBound
+ && lm.getPosition(lastChild) < lm.getItemCount() - 1;
+ return isNewVisibleFromTop || isNewVisibleFromBottom;
+ }
+
+ @Override
+ public int getFlingVelocity(int velocityX, int velocityY) {
+ return velocityY;
+ }
+
+ @Override
+ public boolean canScrollHorizontally() {
+ return false;
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return true;
+ }
+
+ @Override
+ public int getPendingDx(int pendingScroll) {
+ return 0;
+ }
+
+ @Override
+ public int getPendingDy(int pendingScroll) {
+ return pendingScroll;
+ }
+ }
+
+}
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/Direction.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/Direction.java
new file mode 100644
index 0000000000..e4d1c386ce
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/Direction.java
@@ -0,0 +1,38 @@
+package com.yarolegovich.discretescrollview;
+
+/**
+ * Created by yarolegovich on 16.03.2017.
+ */
+enum Direction {
+
+ START {
+ @Override
+ public int applyTo(int delta) {
+ return delta * -1;
+ }
+
+ @Override
+ public boolean sameAs(int direction) {
+ return direction < 0;
+ }
+ },
+ END {
+ @Override
+ public int applyTo(int delta) {
+ return delta;
+ }
+
+ @Override
+ public boolean sameAs(int direction) {
+ return direction > 0;
+ }
+ };
+
+ public abstract int applyTo(int delta);
+
+ public abstract boolean sameAs(int direction);
+
+ public static Direction fromDelta(int delta) {
+ return delta > 0 ? END : START;
+ }
+}
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java
new file mode 100644
index 0000000000..3cc41b2db5
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java
@@ -0,0 +1,786 @@
+package com.yarolegovich.discretescrollview;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.accessibility.AccessibilityEventCompat;
+import androidx.core.view.accessibility.AccessibilityRecordCompat;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
+import com.yarolegovich.discretescrollview.transform.DiscreteScrollItemTransformer;
+
+import java.util.Locale;
+
+/**
+ * Created by yarolegovich on 17.02.2017.
+ */
+class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager {
+
+ static final int NO_POSITION = -1;
+
+ private static final String EXTRA_POSITION = "extra_position";
+ private static final int DEFAULT_TIME_FOR_ITEM_SETTLE = 300;
+ private static final int DEFAULT_FLING_THRESHOLD = 2100; //Decrease to increase sensitivity.
+ private static final int DEFAULT_TRANSFORM_CLAMP_ITEM_COUNT = 1;
+
+ protected static final float SCROLL_TO_SNAP_TO_ANOTHER_ITEM = 0.6f;
+
+ //This field will take value of all visible view's center points during the fill phase
+ protected Point viewCenterIterator;
+ protected Point recyclerCenter;
+ protected Point currentViewCenter;
+ protected int childHalfWidth, childHalfHeight;
+ protected int extraLayoutSpace;
+
+ //Max possible distance a view can travel during one scroll phase
+ protected int scrollToChangeCurrent;
+ protected int currentScrollState;
+
+ protected int scrolled;
+ protected int pendingScroll;
+ protected int currentPosition;
+ protected int pendingPosition;
+
+ protected SparseArray detachedCache;
+
+ private DSVOrientation.Helper orientationHelper;
+
+ protected boolean isFirstOrEmptyLayout;
+
+ private Context context;
+
+ private int timeForItemSettle;
+ private int offscreenItems;
+ private int transformClampItemCount;
+
+ private boolean dataSetChangeShiftedPosition;
+
+ private int flingThreshold;
+ private boolean shouldSlideOnFling;
+
+ private int viewWidth, viewHeight;
+
+ @NonNull
+ private final ScrollStateListener scrollStateListener;
+ private DiscreteScrollItemTransformer itemTransformer;
+
+ private RecyclerViewProxy recyclerViewProxy;
+
+ public DiscreteScrollLayoutManager(
+ @NonNull Context c,
+ @NonNull ScrollStateListener scrollStateListener,
+ @NonNull DSVOrientation orientation) {
+ this.context = c;
+ this.timeForItemSettle = DEFAULT_TIME_FOR_ITEM_SETTLE;
+ this.pendingPosition = NO_POSITION;
+ this.currentPosition = NO_POSITION;
+ this.flingThreshold = DEFAULT_FLING_THRESHOLD;
+ this.shouldSlideOnFling = false;
+ this.recyclerCenter = new Point();
+ this.currentViewCenter = new Point();
+ this.viewCenterIterator = new Point();
+ this.detachedCache = new SparseArray<>();
+ this.scrollStateListener = scrollStateListener;
+ this.orientationHelper = orientation.createHelper();
+ this.recyclerViewProxy = new RecyclerViewProxy(this);
+ this.transformClampItemCount = DEFAULT_TRANSFORM_CLAMP_ITEM_COUNT;
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (state.getItemCount() == 0) {
+ recyclerViewProxy.removeAndRecycleAllViews(recycler);
+ currentPosition = pendingPosition = NO_POSITION;
+ scrolled = pendingScroll = 0;
+ return;
+ }
+
+ ensureValidPosition(state);
+
+ updateRecyclerDimensions(state);
+
+ //onLayoutChildren may be called multiple times and this check is required so that the flag
+ //won't be cleared until onLayoutCompleted
+ if (!isFirstOrEmptyLayout) {
+ isFirstOrEmptyLayout = recyclerViewProxy.getChildCount() == 0;
+ if (isFirstOrEmptyLayout) {
+ initChildDimensions(recycler);
+ }
+ }
+
+ recyclerViewProxy.detachAndScrapAttachedViews(recycler);
+
+ fill(recycler);
+
+ applyItemTransformToChildren();
+ }
+
+ private void ensureValidPosition(RecyclerView.State state) {
+ if (currentPosition == NO_POSITION || currentPosition >= state.getItemCount()) {
+ //currentPosition might have been assigned in onRestoreInstanceState()
+ //which can lead to a crash (position out of bounds) when data set
+ //is not persisted across rotations
+ currentPosition = 0;
+ }
+ }
+
+ @Override
+ public void onLayoutCompleted(RecyclerView.State state) {
+ if (isFirstOrEmptyLayout) {
+ scrollStateListener.onCurrentViewFirstLayout();
+ isFirstOrEmptyLayout = false;
+ } else if (dataSetChangeShiftedPosition) {
+ scrollStateListener.onDataSetChangeChangedPosition();
+ dataSetChangeShiftedPosition = false;
+ }
+ }
+
+ protected void initChildDimensions(RecyclerView.Recycler recycler) {
+ View viewToMeasure = recyclerViewProxy.getMeasuredChildForAdapterPosition(0, recycler);
+
+ int childViewWidth = recyclerViewProxy.getMeasuredWidthWithMargin(viewToMeasure);
+ int childViewHeight = recyclerViewProxy.getMeasuredHeightWithMargin(viewToMeasure);
+
+ childHalfWidth = childViewWidth / 2;
+ childHalfHeight = childViewHeight / 2;
+
+ scrollToChangeCurrent = orientationHelper.getDistanceToChangeCurrent(
+ childViewWidth,
+ childViewHeight);
+
+ extraLayoutSpace = scrollToChangeCurrent * offscreenItems;
+
+ recyclerViewProxy.detachAndScrapView(viewToMeasure, recycler);
+ }
+
+ protected void updateRecyclerDimensions(RecyclerView.State state) {
+ boolean dimensionsChanged = !state.isMeasuring()
+ && (recyclerViewProxy.getWidth() != viewWidth
+ || recyclerViewProxy.getHeight() != viewHeight);
+ if (dimensionsChanged) {
+ viewWidth = recyclerViewProxy.getWidth();
+ viewHeight = recyclerViewProxy.getHeight();
+ recyclerViewProxy.removeAllViews();
+ }
+ recyclerCenter.set(
+ recyclerViewProxy.getWidth() / 523*330,
+ recyclerViewProxy.getHeight() / 2);
+ }
+
+ protected void fill(RecyclerView.Recycler recycler) {
+ cacheAndDetachAttachedViews();
+
+ orientationHelper.setCurrentViewCenter(recyclerCenter, scrolled, currentViewCenter);
+
+ final int endBound = orientationHelper.getViewEnd(
+ recyclerViewProxy.getWidth(),
+ recyclerViewProxy.getHeight());
+
+ //Layout current
+ if (isViewVisible(currentViewCenter, endBound)) {
+ layoutView(recycler, currentPosition, currentViewCenter);
+ }
+
+ //Layout items before the current item
+ layoutViews(recycler, Direction.START, endBound);
+
+ //Layout items after the current item
+ layoutViews(recycler, Direction.END, endBound);
+
+ recycleDetachedViewsAndClearCache(recycler);
+ }
+
+ private void layoutViews(RecyclerView.Recycler recycler, Direction direction, int endBound) {
+ final int positionStep = direction.applyTo(1);
+
+ //Predictive layout is required when we are doing smooth fast scroll towards pendingPosition
+ boolean noPredictiveLayoutRequired = pendingPosition == NO_POSITION
+ || !direction.sameAs(pendingPosition - currentPosition);
+
+ viewCenterIterator.set(currentViewCenter.x, currentViewCenter.y);
+ for (int pos = currentPosition + positionStep; isInBounds(pos); pos += positionStep) {
+ if (pos == pendingPosition) {
+ noPredictiveLayoutRequired = true;
+ }
+ orientationHelper.shiftViewCenter(direction, scrollToChangeCurrent, viewCenterIterator);
+ if (isViewVisible(viewCenterIterator, endBound)) {
+ layoutView(recycler, pos, viewCenterIterator);
+ } else if (noPredictiveLayoutRequired) {
+ break;
+ }
+ }
+ }
+
+ protected void layoutView(RecyclerView.Recycler recycler, int position, Point viewCenter) {
+ if (position < 0) return;
+ View v = detachedCache.get(position);
+ if (v == null) {
+ v = recyclerViewProxy.getMeasuredChildForAdapterPosition(position, recycler);
+ recyclerViewProxy.layoutDecoratedWithMargins(v,
+ viewCenter.x - childHalfWidth, viewCenter.y - childHalfHeight,
+ viewCenter.x + childHalfWidth, viewCenter.y + childHalfHeight);
+ } else {
+ recyclerViewProxy.attachView(v);
+ detachedCache.remove(position);
+ }
+ }
+
+ protected void cacheAndDetachAttachedViews() {
+ detachedCache.clear();
+ for (int i = 0; i < recyclerViewProxy.getChildCount(); i++) {
+ View child = recyclerViewProxy.getChildAt(i);
+ detachedCache.put(recyclerViewProxy.getPosition(child), child);
+ }
+
+ for (int i = 0; i < detachedCache.size(); i++) {
+ recyclerViewProxy.detachView(detachedCache.valueAt(i));
+ }
+ }
+
+ protected void recycleDetachedViewsAndClearCache(RecyclerView.Recycler recycler) {
+ for (int i = 0; i < detachedCache.size(); i++) {
+ View viewToRemove = detachedCache.valueAt(i);
+ recyclerViewProxy.recycleView(viewToRemove, recycler);
+ }
+ detachedCache.clear();
+ }
+
+ @Override
+ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+ int newPosition = currentPosition;
+ if (currentPosition == NO_POSITION) {
+ newPosition = 0;
+ } else if (currentPosition >= positionStart) {
+ newPosition = Math.min(currentPosition + itemCount, recyclerViewProxy.getItemCount() - 1);
+ }
+ onNewPosition(newPosition);
+ }
+
+ @Override
+ public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
+ int newPosition = currentPosition;
+ if (recyclerViewProxy.getItemCount() == 0) {
+ newPosition = NO_POSITION;
+ } else if (currentPosition >= positionStart) {
+ if (currentPosition < positionStart + itemCount) {
+ //If currentPosition is in the removed items, then the new item became current
+ currentPosition = NO_POSITION;
+ }
+ newPosition = Math.max(0, currentPosition - itemCount);
+ }
+ onNewPosition(newPosition);
+ }
+
+ @Override
+ public void onItemsChanged(RecyclerView recyclerView) {
+ //notifyDataSetChanged() was called. We need to ensure that currentPosition is not out of bounds
+ currentPosition = Math.min(Math.max(0, currentPosition), recyclerViewProxy.getItemCount() - 1);
+ dataSetChangeShiftedPosition = true;
+ }
+
+ private void onNewPosition(int position) {
+ if (currentPosition != position) {
+ currentPosition = position;
+ dataSetChangeShiftedPosition = true;
+ }
+ }
+
+ @Override
+ public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ return scrollBy(dx, recycler);
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ return scrollBy(dy, recycler);
+ }
+
+ protected int scrollBy(int amount, RecyclerView.Recycler recycler) {
+ if (recyclerViewProxy.getChildCount() == 0) {
+ return 0;
+ }
+
+ Direction direction = Direction.fromDelta(amount);
+ int leftToScroll = calculateAllowedScrollIn(direction);
+ if (leftToScroll <= 0) {
+ return 0;
+ }
+
+ int delta = direction.applyTo(Math.min(leftToScroll, Math.abs(amount)));
+ scrolled += delta;
+ if (pendingScroll != 0) {
+ pendingScroll -= delta;
+ }
+
+ orientationHelper.offsetChildren(-delta, recyclerViewProxy);
+
+ if (orientationHelper.hasNewBecomeVisible(this)) {
+ fill(recycler);
+ }
+
+ notifyScroll();
+
+ applyItemTransformToChildren();
+
+ return delta;
+ }
+
+ protected void applyItemTransformToChildren() {
+ if (itemTransformer != null) {
+ int clampAfterDistance = scrollToChangeCurrent * transformClampItemCount;
+ for (int i = 0; i < recyclerViewProxy.getChildCount(); i++) {
+ View child = recyclerViewProxy.getChildAt(i);
+ float position = getCenterRelativePositionOf(child, clampAfterDistance);
+ itemTransformer.transformItem(child, position);
+ }
+ }
+ }
+
+ @Override
+ public void scrollToPosition(int position) {
+ if (currentPosition == position) {
+ return;
+ }
+
+ currentPosition = position;
+ recyclerViewProxy.requestLayout();
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
+ if (currentPosition == position || pendingPosition != NO_POSITION) {
+ return;
+ }
+ checkTargetPosition(state, position);
+ if (currentPosition == NO_POSITION) {
+ //Layout not happened yet
+ currentPosition = position;
+ } else {
+ startSmoothPendingScroll(position);
+ }
+ }
+
+ @Override
+ public boolean canScrollHorizontally() {
+ return orientationHelper.canScrollHorizontally();
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return orientationHelper.canScrollVertically();
+ }
+
+ @Override
+ public void onScrollStateChanged(int state) {
+ if (currentScrollState == RecyclerView.SCROLL_STATE_IDLE && currentScrollState != state) {
+ scrollStateListener.onScrollStart();
+ }
+
+ if (state == RecyclerView.SCROLL_STATE_IDLE) {
+ //Scroll is not finished until current view is centered
+ boolean isScrollEnded = onScrollEnd();
+ if (isScrollEnded) {
+ scrollStateListener.onScrollEnd();
+ } else {
+ //Scroll continues and we don't want to set currentScrollState to STATE_IDLE,
+ //because this will then trigger .scrollStateListener.onScrollStart()
+ return;
+ }
+ } else if (state == RecyclerView.SCROLL_STATE_DRAGGING) {
+ onDragStart();
+ }
+ currentScrollState = state;
+ }
+
+ /**
+ * @return true if scroll is ended and we don't need to settle items
+ */
+ private boolean onScrollEnd() {
+ if (pendingPosition != NO_POSITION) {
+ currentPosition = pendingPosition;
+ pendingPosition = NO_POSITION;
+ scrolled = 0;
+ }
+
+ Direction scrollDirection = Direction.fromDelta(scrolled);
+ if (Math.abs(scrolled) == scrollToChangeCurrent) {
+ currentPosition += scrollDirection.applyTo(1);
+ scrolled = 0;
+ }
+
+ if (isAnotherItemCloserThanCurrent()) {
+ pendingScroll = getHowMuchIsLeftToScroll(scrolled);
+ } else {
+ pendingScroll = -scrolled;
+ }
+
+ if (pendingScroll == 0) {
+ return true;
+ } else {
+ startSmoothPendingScroll();
+ return false;
+ }
+ }
+
+ private void onDragStart() {
+ //Here we need to:
+ //1. Stop any pending scroll
+ //2. Set currentPosition to position of the item that is closest to the center
+ boolean isScrollingThroughMultiplePositions = Math.abs(scrolled) > scrollToChangeCurrent;
+ if (isScrollingThroughMultiplePositions) {
+ int scrolledPositions = scrolled / scrollToChangeCurrent;
+ currentPosition += scrolledPositions;
+ scrolled -= scrolledPositions * scrollToChangeCurrent;
+ }
+ if (isAnotherItemCloserThanCurrent()) {
+ Direction direction = Direction.fromDelta(scrolled);
+ currentPosition += direction.applyTo(1);
+ scrolled = -getHowMuchIsLeftToScroll(scrolled);
+ }
+ pendingPosition = NO_POSITION;
+ pendingScroll = 0;
+ }
+
+ public void onFling(int velocityX, int velocityY) {
+ int velocity = orientationHelper.getFlingVelocity(velocityX, velocityY);
+ int throttleValue = shouldSlideOnFling ? Math.abs(velocity / flingThreshold) : 1;
+ int newPosition = currentPosition + Direction.fromDelta(velocity).applyTo(throttleValue);
+ newPosition = checkNewOnFlingPositionIsInBounds(newPosition);
+ boolean isInScrollDirection = velocity * scrolled >= 0;
+ boolean canFling = isInScrollDirection && isInBounds(newPosition);
+ if (canFling) {
+ startSmoothPendingScroll(newPosition);
+ } else {
+ returnToCurrentPosition();
+ }
+ }
+
+ public void returnToCurrentPosition() {
+ pendingScroll = -scrolled;
+ if (pendingScroll != 0) {
+ startSmoothPendingScroll();
+ }
+ }
+
+ protected int calculateAllowedScrollIn(Direction direction) {
+ if (pendingScroll != 0) {
+ return Math.abs(pendingScroll);
+ }
+ int allowedScroll;
+ boolean isBoundReached;
+ boolean isScrollDirectionAsBefore = direction.applyTo(scrolled) > 0;
+ if (direction == Direction.START && currentPosition == 0) {
+ //We can scroll to the left when currentPosition == 0 only if we scrolled to the right before
+ isBoundReached = scrolled == 0;
+ allowedScroll = isBoundReached ? 0 : Math.abs(scrolled);
+ } else if (direction == Direction.END && currentPosition == recyclerViewProxy.getItemCount() - 1) {
+ //We can scroll to the right when currentPosition == last only if we scrolled to the left before
+ isBoundReached = scrolled == 0;
+ allowedScroll = isBoundReached ? 0 : Math.abs(scrolled);
+ } else {
+ isBoundReached = false;
+ allowedScroll = isScrollDirectionAsBefore ?
+ scrollToChangeCurrent - Math.abs(scrolled) :
+ scrollToChangeCurrent + Math.abs(scrolled);
+ }
+ scrollStateListener.onIsBoundReachedFlagChange(isBoundReached);
+ return allowedScroll;
+ }
+
+ private void startSmoothPendingScroll() {
+ LinearSmoothScroller scroller = new DiscreteLinearSmoothScroller(context);
+ scroller.setTargetPosition(currentPosition);
+ recyclerViewProxy.startSmoothScroll(scroller);
+ }
+
+ private void startSmoothPendingScroll(int position) {
+ if (currentPosition == position) return;
+ pendingScroll = -scrolled;
+ Direction direction = Direction.fromDelta(position - currentPosition);
+ int distanceToScroll = Math.abs(position - currentPosition) * scrollToChangeCurrent;
+ pendingScroll += direction.applyTo(distanceToScroll);
+ pendingPosition = position;
+ startSmoothPendingScroll();
+ }
+
+ @Override
+ public boolean isAutoMeasureEnabled() {
+ return true;
+ }
+
+ @Override
+ public int computeVerticalScrollRange(RecyclerView.State state) {
+ return computeScrollRange(state);
+ }
+
+ @Override
+ public int computeVerticalScrollOffset(RecyclerView.State state) {
+ return computeScrollOffset(state);
+ }
+
+ @Override
+ public int computeVerticalScrollExtent(RecyclerView.State state) {
+ return computeScrollExtent(state);
+ }
+
+ @Override
+ public int computeHorizontalScrollRange(RecyclerView.State state) {
+ return computeScrollRange(state);
+ }
+
+ @Override
+ public int computeHorizontalScrollOffset(RecyclerView.State state) {
+ return computeScrollOffset(state);
+ }
+
+ @Override
+ public int computeHorizontalScrollExtent(RecyclerView.State state) {
+ return computeScrollExtent(state);
+ }
+
+ private int computeScrollOffset(RecyclerView.State state) {
+ int scrollbarSize = computeScrollExtent(state);
+ int offset = (int) ((scrolled / (float) scrollToChangeCurrent) * scrollbarSize);
+ return (currentPosition * scrollbarSize) + offset;
+ }
+
+ private int computeScrollExtent(RecyclerView.State state) {
+ if (getItemCount() == 0) {
+ return 0;
+ } else {
+ return (int) (computeScrollRange(state) / (float) getItemCount());
+ }
+ }
+
+ private int computeScrollRange(RecyclerView.State state) {
+ if (getItemCount() == 0) {
+ return 0;
+ } else {
+ return scrollToChangeCurrent * (getItemCount() - 1);
+ }
+ }
+
+ @Override
+ public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
+ pendingPosition = NO_POSITION;
+ scrolled = pendingScroll = 0;
+ if (newAdapter instanceof InitialPositionProvider) {
+ currentPosition = ((InitialPositionProvider) newAdapter).getInitialPosition();
+ } else {
+ currentPosition = 0;
+ }
+ recyclerViewProxy.removeAllViews();
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Bundle bundle = new Bundle();
+ if (pendingPosition != NO_POSITION) {
+ currentPosition = pendingPosition;
+ }
+ bundle.putInt(EXTRA_POSITION, currentPosition);
+ return bundle;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ Bundle bundle = (Bundle) state;
+ currentPosition = bundle.getInt(EXTRA_POSITION);
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new RecyclerView.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ public int getNextPosition() {
+ if (scrolled == 0) {
+ return currentPosition;
+ } else if (pendingPosition != NO_POSITION) {
+ return pendingPosition;
+ } else {
+ return currentPosition + Direction.fromDelta(scrolled).applyTo(1);
+ }
+ }
+
+ public void setItemTransformer(DiscreteScrollItemTransformer itemTransformer) {
+ this.itemTransformer = itemTransformer;
+ }
+
+ public void setTimeForItemSettle(int timeForItemSettle) {
+ this.timeForItemSettle = timeForItemSettle;
+ }
+
+ public void setOffscreenItems(int offscreenItems) {
+ this.offscreenItems = offscreenItems;
+ extraLayoutSpace = scrollToChangeCurrent * offscreenItems;
+ recyclerViewProxy.requestLayout();
+ }
+
+ public void setTransformClampItemCount(int transformClampItemCount) {
+ this.transformClampItemCount = transformClampItemCount;
+ applyItemTransformToChildren();
+ }
+
+ public void setOrientation(DSVOrientation orientation) {
+ orientationHelper = orientation.createHelper();
+ recyclerViewProxy.removeAllViews();
+ recyclerViewProxy.requestLayout();
+ }
+
+ public void setShouldSlideOnFling(boolean result) {
+ shouldSlideOnFling = result;
+ }
+
+ public void setSlideOnFlingThreshold(int threshold) {
+ flingThreshold = threshold;
+ }
+
+ public int getCurrentPosition() {
+ return currentPosition;
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ if (recyclerViewProxy.getChildCount() > 0) {
+ final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
+ record.setFromIndex(getPosition(getFirstChild()));
+ record.setToIndex(getPosition(getLastChild()));
+ }
+ }
+
+ private float getCenterRelativePositionOf(View v, int maxDistance) {
+ float distanceFromCenter = orientationHelper.getDistanceFromCenter(recyclerCenter,
+ getDecoratedLeft(v) + childHalfWidth,
+ getDecoratedTop(v) + childHalfHeight);
+ return Math.min(Math.max(-1f, distanceFromCenter / maxDistance), 1f);
+ }
+
+ private int checkNewOnFlingPositionIsInBounds(int position) {
+ final int itemCount = recyclerViewProxy.getItemCount();
+ //The check is required in case slide through multiple items is turned on
+ if (currentPosition != 0 && position < 0) {
+ //If currentPosition == 0 && position < 0 we forbid scroll to the left,
+ //but if currentPosition != 0 we can slide to the first item
+ return 0;
+ } else if (currentPosition != itemCount - 1 && position >= itemCount) {
+ return itemCount - 1;
+ }
+ return position;
+ }
+
+ private int getHowMuchIsLeftToScroll(int dx) {
+ return Direction.fromDelta(dx).applyTo(scrollToChangeCurrent - Math.abs(scrolled));
+ }
+
+ private boolean isAnotherItemCloserThanCurrent() {
+ return Math.abs(scrolled) >= scrollToChangeCurrent * SCROLL_TO_SNAP_TO_ANOTHER_ITEM;
+ }
+
+ public View getFirstChild() {
+ return recyclerViewProxy.getChildAt(0);
+ }
+
+ public View getLastChild() {
+ return recyclerViewProxy.getChildAt(recyclerViewProxy.getChildCount() - 1);
+ }
+
+ public int getExtraLayoutSpace() {
+ return extraLayoutSpace;
+ }
+
+ private void notifyScroll() {
+ float amountToScroll = pendingPosition != NO_POSITION ?
+ Math.abs(scrolled + pendingScroll) :
+ scrollToChangeCurrent;
+ float position = -Math.min(Math.max(-1f, scrolled / amountToScroll), 1f);
+ scrollStateListener.onScroll(position);
+ }
+
+ private boolean isInBounds(int itemPosition) {
+ return itemPosition >= 0 && itemPosition < recyclerViewProxy.getItemCount();
+ }
+
+ private boolean isViewVisible(Point viewCenter, int endBound) {
+ return orientationHelper.isViewVisible(
+ viewCenter, childHalfWidth, childHalfHeight,
+ endBound, extraLayoutSpace);
+ }
+
+ private void checkTargetPosition(RecyclerView.State state, int targetPosition) {
+ if (targetPosition < 0 || targetPosition >= state.getItemCount()) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "target position out of bounds: position=%d, itemCount=%d",
+ targetPosition, state.getItemCount()));
+ }
+ }
+
+ protected void setRecyclerViewProxy(RecyclerViewProxy recyclerViewProxy) {
+ this.recyclerViewProxy = recyclerViewProxy;
+ }
+
+ protected void setOrientationHelper(DSVOrientation.Helper orientationHelper) {
+ this.orientationHelper = orientationHelper;
+ }
+
+ private class DiscreteLinearSmoothScroller extends LinearSmoothScroller {
+
+ public DiscreteLinearSmoothScroller(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int calculateDxToMakeVisible(View view, int snapPreference) {
+ return orientationHelper.getPendingDx(-pendingScroll);
+ }
+
+ @Override
+ public int calculateDyToMakeVisible(View view, int snapPreference) {
+ return orientationHelper.getPendingDy(-pendingScroll);
+ }
+
+ @Override
+ protected int calculateTimeForScrolling(int dx) {
+ float dist = Math.min(Math.abs(dx), scrollToChangeCurrent);
+ return (int) (Math.max(0.01f, dist / scrollToChangeCurrent) * timeForItemSettle);
+ }
+
+ @Nullable
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ return new PointF(
+ orientationHelper.getPendingDx(pendingScroll),
+ orientationHelper.getPendingDy(pendingScroll));
+ }
+ }
+
+ public interface ScrollStateListener {
+ void onIsBoundReachedFlagChange(boolean isBoundReached);
+
+ void onScrollStart();
+
+ void onScrollEnd();
+
+ void onScroll(float currentViewPosition);
+
+ void onCurrentViewFirstLayout();
+
+ void onDataSetChangeChangedPosition();
+ }
+
+ public interface InitialPositionProvider {
+ int getInitialPosition();
+ }
+}
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java
new file mode 100644
index 0000000000..27973063b8
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java
@@ -0,0 +1,293 @@
+package com.yarolegovich.discretescrollview;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import com.yarolegovich.discretescrollview.transform.DiscreteScrollItemTransformer;
+import com.yarolegovich.discretescrollview.util.ScrollListenerAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by yarolegovich on 18.02.2017.
+ */
+@SuppressWarnings("unchecked")
+public class DiscreteScrollView extends RecyclerView {
+
+ public static final int NO_POSITION = DiscreteScrollLayoutManager.NO_POSITION;
+
+ private static final int DEFAULT_ORIENTATION = DSVOrientation.HORIZONTAL.ordinal();
+
+ private DiscreteScrollLayoutManager layoutManager;
+
+ private List scrollStateChangeListeners;
+ private List onItemChangedListeners;
+
+ private boolean isOverScrollEnabled;
+
+ public DiscreteScrollView(Context context) {
+ super(context);
+ init(null);
+ }
+
+ public DiscreteScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs);
+ }
+
+ public DiscreteScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(attrs);
+ }
+
+ private void init(AttributeSet attrs) {
+ scrollStateChangeListeners = new ArrayList<>();
+ onItemChangedListeners = new ArrayList<>();
+
+ int orientation = DEFAULT_ORIENTATION;
+ if (attrs != null) {
+ TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.DiscreteScrollView);
+ orientation = ta.getInt(R.styleable.DiscreteScrollView_dsv_orientation, DEFAULT_ORIENTATION);
+ ta.recycle();
+ }
+
+ isOverScrollEnabled = getOverScrollMode() != OVER_SCROLL_NEVER;
+
+ layoutManager = new DiscreteScrollLayoutManager(
+ getContext(), new ScrollStateListener(),
+ DSVOrientation.values()[orientation]);
+ setLayoutManager(layoutManager);
+ }
+
+ @Override
+ public void setLayoutManager(LayoutManager layout) {
+ if (layout instanceof DiscreteScrollLayoutManager) {
+ super.setLayoutManager(layout);
+ } else {
+ throw new IllegalArgumentException(getContext().getString(R.string.dsv_ex_msg_dont_set_lm));
+ }
+ }
+
+
+ @Override
+ public boolean fling(int velocityX, int velocityY) {
+ boolean isFling = super.fling(velocityX, velocityY);
+ if (isFling) {
+ layoutManager.onFling(velocityX, velocityY);
+ } else {
+ layoutManager.returnToCurrentPosition();
+ }
+ return isFling;
+ }
+
+ @Nullable
+ public ViewHolder getViewHolder(int position) {
+ View view = layoutManager.findViewByPosition(position);
+ return view != null ? getChildViewHolder(view) : null;
+ }
+
+ /**
+ * @return adapter position of the current item or -1 if nothing is selected
+ */
+ public int getCurrentItem() {
+ return layoutManager.getCurrentPosition();
+ }
+
+ public void setItemTransformer(DiscreteScrollItemTransformer transformer) {
+ layoutManager.setItemTransformer(transformer);
+ }
+
+ public void setItemTransitionTimeMillis(@IntRange(from = 10) int millis) {
+ layoutManager.setTimeForItemSettle(millis);
+ }
+
+ public void setSlideOnFling(boolean result){
+ layoutManager.setShouldSlideOnFling(result);
+ }
+
+ public void setSlideOnFlingThreshold(int threshold){
+ layoutManager.setSlideOnFlingThreshold(threshold);
+ }
+
+ public void setOrientation(DSVOrientation orientation) {
+ layoutManager.setOrientation(orientation);
+ }
+
+ public void setOffscreenItems(int items) {
+ layoutManager.setOffscreenItems(items);
+ }
+
+ public void setClampTransformProgressAfter(@IntRange(from = 1) int itemCount) {
+ if (itemCount <= 1) {
+ throw new IllegalArgumentException("must be >= 1");
+ }
+ layoutManager.setTransformClampItemCount(itemCount);
+ }
+
+ public void setOverScrollEnabled(boolean overScrollEnabled) {
+ isOverScrollEnabled = overScrollEnabled;
+ setOverScrollMode(OVER_SCROLL_NEVER);
+ }
+
+ public void addScrollStateChangeListener(@NonNull ScrollStateChangeListener> scrollStateChangeListener) {
+ scrollStateChangeListeners.add(scrollStateChangeListener);
+ }
+
+ public void addScrollListener(@NonNull ScrollListener> scrollListener) {
+ addScrollStateChangeListener(new ScrollListenerAdapter(scrollListener));
+ }
+
+ public void addOnItemChangedListener(@NonNull OnItemChangedListener> onItemChangedListener) {
+ onItemChangedListeners.add(onItemChangedListener);
+ }
+
+ public void removeScrollStateChangeListener(@NonNull ScrollStateChangeListener> scrollStateChangeListener) {
+ scrollStateChangeListeners.remove(scrollStateChangeListener);
+ }
+
+ public void removeScrollListener(@NonNull ScrollListener> scrollListener) {
+ removeScrollStateChangeListener(new ScrollListenerAdapter<>(scrollListener));
+ }
+
+ public void removeItemChangedListener(@NonNull OnItemChangedListener> onItemChangedListener) {
+ onItemChangedListeners.remove(onItemChangedListener);
+ }
+
+ private void notifyScrollStart(ViewHolder holder, int current) {
+ for (ScrollStateChangeListener listener : scrollStateChangeListeners) {
+ listener.onScrollStart(holder, current);
+ }
+ }
+
+ private void notifyScrollEnd(ViewHolder holder, int current) {
+ for (ScrollStateChangeListener listener : scrollStateChangeListeners) {
+ listener.onScrollEnd(holder, current);
+ }
+ }
+
+ private void notifyScroll(float position,
+ int currentIndex, int newIndex,
+ ViewHolder currentHolder, ViewHolder newHolder) {
+ for (ScrollStateChangeListener listener : scrollStateChangeListeners) {
+ listener.onScroll(position, currentIndex, newIndex,
+ currentHolder,
+ newHolder);
+ }
+ }
+
+ private void notifyCurrentItemChanged(ViewHolder holder, int current) {
+ for (OnItemChangedListener listener : onItemChangedListeners) {
+ listener.onCurrentItemChanged(holder, current);
+ }
+ }
+
+ private void notifyCurrentItemChanged() {
+ if (onItemChangedListeners.isEmpty()) {
+ return;
+ }
+ int current = layoutManager.getCurrentPosition();
+ ViewHolder currentHolder = getViewHolder(current);
+ notifyCurrentItemChanged(currentHolder, current);
+ }
+
+ private class ScrollStateListener implements DiscreteScrollLayoutManager.ScrollStateListener {
+
+ @Override
+ public void onIsBoundReachedFlagChange(boolean isBoundReached) {
+ if (isOverScrollEnabled) {
+ setOverScrollMode(isBoundReached ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER);
+ }
+ }
+
+ @Override
+ public void onScrollStart() {
+ if (scrollStateChangeListeners.isEmpty()) {
+ return;
+ }
+ int current = layoutManager.getCurrentPosition();
+ ViewHolder holder = getViewHolder(current);
+ if (holder != null) {
+ notifyScrollStart(holder, current);
+ }
+ }
+
+ @Override
+ public void onScrollEnd() {
+ if (onItemChangedListeners.isEmpty() && scrollStateChangeListeners.isEmpty()) {
+ return;
+ }
+ int current = layoutManager.getCurrentPosition();
+ ViewHolder holder = getViewHolder(current);
+ if (holder != null) {
+ notifyScrollEnd(holder, current);
+ notifyCurrentItemChanged(holder, current);
+ }
+ }
+
+ @Override
+ public void onScroll(float currentViewPosition) {
+ if (scrollStateChangeListeners.isEmpty()) {
+ return;
+ }
+ int currentIndex = getCurrentItem();
+ int newIndex = layoutManager.getNextPosition();
+ if (currentIndex != newIndex) {
+ notifyScroll(currentViewPosition,
+ currentIndex, newIndex,
+ getViewHolder(currentIndex),
+ getViewHolder(newIndex));
+ }
+ }
+
+ @Override
+ public void onCurrentViewFirstLayout() {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ notifyCurrentItemChanged();
+ }
+ });
+ }
+
+ @Override
+ public void onDataSetChangeChangedPosition() {
+ notifyCurrentItemChanged();
+ }
+ }
+
+ public interface ScrollStateChangeListener {
+
+ void onScrollStart(@NonNull T currentItemHolder, int adapterPosition);
+
+ void onScrollEnd(@NonNull T currentItemHolder, int adapterPosition);
+
+ void onScroll(float scrollPosition,
+ int currentPosition,
+ int newPosition,
+ @Nullable T currentHolder,
+ @Nullable T newCurrent);
+ }
+
+ public interface ScrollListener {
+
+ void onScroll(float scrollPosition,
+ int currentPosition, int newPosition,
+ @Nullable T currentHolder,
+ @Nullable T newCurrent);
+ }
+
+ public interface OnItemChangedListener {
+ /*
+ * This method will be also triggered when view appears on the screen for the first time.
+ * If data set is empty, viewHolder will be null and adapterPosition will be NO_POSITION
+ */
+ void onCurrentItemChanged(@Nullable T viewHolder, int adapterPosition);
+ }
+}
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java
new file mode 100644
index 0000000000..49cdb7ef45
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java
@@ -0,0 +1,178 @@
+package com.yarolegovich.discretescrollview;
+
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import java.util.Locale;
+
+/**
+ * Created by yarolegovich on 28-Apr-17.
+ */
+
+public class InfiniteScrollAdapter extends RecyclerView.Adapter
+ implements DiscreteScrollLayoutManager.InitialPositionProvider {
+
+ private static final int CENTER = Integer.MAX_VALUE / 2;
+ private static final int RESET_BOUND = 100;
+
+ public static InfiniteScrollAdapter wrap(
+ @NonNull RecyclerView.Adapter adapter) {
+ return new InfiniteScrollAdapter<>(adapter);
+ }
+
+ private RecyclerView.Adapter wrapped;
+ private DiscreteScrollLayoutManager layoutManager;
+
+ public InfiniteScrollAdapter(@NonNull RecyclerView.Adapter wrapped) {
+ this.wrapped = wrapped;
+ this.wrapped.registerAdapterDataObserver(new DataSetChangeDelegate());
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ wrapped.onAttachedToRecyclerView(recyclerView);
+ if (recyclerView instanceof DiscreteScrollView) {
+ layoutManager = (DiscreteScrollLayoutManager) recyclerView.getLayoutManager();
+ } else {
+ String msg = recyclerView.getContext().getString(R.string.dsv_ex_msg_adapter_wrong_recycler);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ wrapped.onDetachedFromRecyclerView(recyclerView);
+ layoutManager = null;
+ }
+
+ @Override
+ public @NonNull T onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return wrapped.onCreateViewHolder(parent, viewType);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull T holder, int position) {
+ if (isResetRequired(position)) {
+ int resetPosition = CENTER + mapPositionToReal(layoutManager.getCurrentPosition());
+ setPosition(resetPosition);
+ return;
+ }
+ wrapped.onBindViewHolder(holder, mapPositionToReal(position));
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return wrapped.getItemViewType(mapPositionToReal(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return isInfinite() ? Integer.MAX_VALUE : wrapped.getItemCount();
+ }
+
+ public int getRealItemCount() {
+ return wrapped.getItemCount();
+ }
+
+ public int getRealCurrentPosition() {
+ return getRealPosition(layoutManager.getCurrentPosition());
+ }
+
+ public int getRealPosition(int position) {
+ return mapPositionToReal(position);
+ }
+
+ public int getClosestPosition(int position) {
+ ensureValidPosition(position);
+ int adapterCurrent = layoutManager.getCurrentPosition();
+ int current = mapPositionToReal(adapterCurrent);
+ if (position == current) {
+ return adapterCurrent;
+ }
+ int delta = position - current;
+ int target = adapterCurrent + delta;
+ int wraparoundTarget = adapterCurrent + (position > current ?
+ delta - wrapped.getItemCount() :
+ wrapped.getItemCount() + delta);
+ int distance = Math.abs(adapterCurrent - target);
+ int wraparoundDistance = Math.abs(adapterCurrent - wraparoundTarget);
+ if (distance == wraparoundDistance) {
+ //Scroll to the right feels more natural, so prefer it
+ return target > adapterCurrent ? target : wraparoundTarget;
+ } else {
+ return distance < wraparoundDistance ? target : wraparoundTarget;
+ }
+ }
+
+ private int mapPositionToReal(int position) {
+ if (position < CENTER) {
+ int rem = (CENTER - position) % wrapped.getItemCount();
+ return rem == 0 ? 0 : wrapped.getItemCount() - rem;
+ } else {
+ return (position - CENTER) % wrapped.getItemCount();
+ }
+ }
+
+ private boolean isResetRequired(int requestedPosition) {
+ return isInfinite()
+ && (requestedPosition <= RESET_BOUND
+ || requestedPosition >= (Integer.MAX_VALUE - RESET_BOUND));
+ }
+
+ private void ensureValidPosition(int position) {
+ if (position >= wrapped.getItemCount()) {
+ throw new IndexOutOfBoundsException(String.format(Locale.US,
+ "requested position is outside adapter's bounds: position=%d, size=%d",
+ position, wrapped.getItemCount()));
+ }
+ }
+
+ private boolean isInfinite() {
+ return wrapped.getItemCount() > 1;
+ }
+
+ @Override
+ public int getInitialPosition() {
+ return isInfinite() ? CENTER : 0;
+ }
+
+ private void setPosition(int position) {
+ layoutManager.scrollToPosition(position);
+ }
+
+ //TODO: handle proper data set change notifications
+ private class DataSetChangeDelegate extends RecyclerView.AdapterDataObserver {
+
+ @Override
+ public void onChanged() {
+ setPosition(getInitialPosition());
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ onChanged();
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ onChanged();
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ onChanged();
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ notifyItemRangeChanged(0, getItemCount());
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ notifyItemRangeChanged(0, getItemCount(), payload);
+ }
+ }
+}
\ No newline at end of file
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java
new file mode 100644
index 0000000000..cbafbf4f6b
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java
@@ -0,0 +1,107 @@
+package com.yarolegovich.discretescrollview;
+
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Created by yarolegovich on 10/25/17.
+ */
+public class RecyclerViewProxy {
+
+ private RecyclerView.LayoutManager layoutManager;
+
+ public RecyclerViewProxy(@NonNull RecyclerView.LayoutManager layoutManager) {
+ this.layoutManager = layoutManager;
+ }
+
+ public void attachView(View view) {
+ layoutManager.attachView(view);
+ }
+
+ public void detachView(View view) {
+ layoutManager.detachView(view);
+ }
+
+ public void detachAndScrapView(View view, RecyclerView.Recycler recycler) {
+ layoutManager.detachAndScrapView(view, recycler);
+ }
+
+ public void detachAndScrapAttachedViews(RecyclerView.Recycler recycler) {
+ layoutManager.detachAndScrapAttachedViews(recycler);
+ }
+
+ public void recycleView(View view, RecyclerView.Recycler recycler) {
+ recycler.recycleView(view);
+ }
+
+ public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
+ layoutManager.removeAndRecycleAllViews(recycler);
+ }
+
+ public int getChildCount() {
+ return layoutManager.getChildCount();
+ }
+
+ public int getItemCount() {
+ return layoutManager.getItemCount();
+ }
+
+ public View getMeasuredChildForAdapterPosition(int position, RecyclerView.Recycler recycler) {
+ View view = recycler.getViewForPosition(position);
+ layoutManager.addView(view);
+ layoutManager.measureChildWithMargins(view, 0, 0);
+ return view;
+ }
+
+ public void layoutDecoratedWithMargins(View v, int left, int top, int right, int bottom) {
+ layoutManager.layoutDecoratedWithMargins(v, left, top, right, bottom);
+ }
+
+ public View getChildAt(int index) {
+ return layoutManager.getChildAt(index);
+ }
+
+ public int getPosition(View view) {
+ return layoutManager.getPosition(view);
+ }
+
+ public int getMeasuredWidthWithMargin(View child) {
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
+ return layoutManager.getDecoratedMeasuredWidth(child) + lp.leftMargin + lp.rightMargin;
+ }
+
+ public int getMeasuredHeightWithMargin(View child) {
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
+ return layoutManager.getDecoratedMeasuredHeight(child) + lp.topMargin + lp.bottomMargin;
+ }
+
+ public int getWidth() {
+ return layoutManager.getWidth();
+ }
+
+ public int getHeight() {
+ return layoutManager.getHeight();
+ }
+
+ public void offsetChildrenHorizontal(int amount) {
+ layoutManager.offsetChildrenHorizontal(amount);
+ }
+
+ public void offsetChildrenVertical(int amount) {
+ layoutManager.offsetChildrenVertical(amount);
+ }
+
+ public void requestLayout() {
+ layoutManager.requestLayout();
+ }
+
+ public void startSmoothScroll(RecyclerView.SmoothScroller smoothScroller) {
+ layoutManager.startSmoothScroll(smoothScroller);
+ }
+
+ public void removeAllViews() {
+ layoutManager.removeAllViews();
+ }
+}
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/transform/DiscreteScrollItemTransformer.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/transform/DiscreteScrollItemTransformer.java
new file mode 100644
index 0000000000..957ae51f41
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/transform/DiscreteScrollItemTransformer.java
@@ -0,0 +1,11 @@
+package com.yarolegovich.discretescrollview.transform;
+
+import android.view.View;
+
+/**
+ * Created by yarolegovich on 02.03.2017.
+ */
+
+public interface DiscreteScrollItemTransformer {
+ void transformItem(View item, float position);
+}
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/transform/Pivot.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/transform/Pivot.java
new file mode 100644
index 0000000000..fc5fcd11d2
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/transform/Pivot.java
@@ -0,0 +1,116 @@
+package com.yarolegovich.discretescrollview.transform;
+
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Created by yarolegovich on 03.03.2017.
+ */
+
+public class Pivot {
+
+ public static final int AXIS_X = 0;
+ public static final int AXIS_Y = 1;
+
+ private static final int PIVOT_CENTER = -1;
+ private static final int PIVOT_MAX = -2;
+
+ private int axis;
+ private int pivotPoint;
+
+ public Pivot(@Axis int axis, int pivotPoint) {
+ this.axis = axis;
+ this.pivotPoint = pivotPoint;
+ }
+
+ public void setOn(View view) {
+ if (axis == AXIS_X) {
+ switch (pivotPoint) {
+ case PIVOT_CENTER:
+ view.setPivotX(view.getWidth() * 0.5f);
+ break;
+ case PIVOT_MAX:
+ view.setPivotX(view.getWidth());
+ break;
+ default:
+ view.setPivotX(pivotPoint);
+ break;
+ }
+ return;
+ }
+
+ if (axis == AXIS_Y) {
+ switch (pivotPoint) {
+ case PIVOT_CENTER:
+ view.setPivotY(view.getHeight() * 0.5f);
+ break;
+ case PIVOT_MAX:
+ view.setPivotY(view.getHeight());
+ break;
+ default:
+ view.setPivotY(pivotPoint);
+ break;
+ }
+ }
+ }
+
+ @Axis
+ public int getAxis() {
+ return axis;
+ }
+
+ public enum X {
+ LEFT {
+ @Override
+ public Pivot create() {
+ return new Pivot(AXIS_X, 0);
+ }
+ },
+ CENTER {
+ @Override
+ public Pivot create() {
+ return new Pivot(AXIS_X, PIVOT_CENTER);
+ }
+ },
+ RIGHT {
+ @Override
+ public Pivot create() {
+ return new Pivot(AXIS_X, PIVOT_MAX);
+ }
+ };
+
+ public abstract Pivot create();
+ }
+
+ public enum Y {
+ TOP {
+ @Override
+ public Pivot create() {
+ return new Pivot(AXIS_Y, 0);
+ }
+ },
+ CENTER {
+ @Override
+ public Pivot create() {
+ return new Pivot(AXIS_Y, PIVOT_CENTER);
+ }
+ },
+ BOTTOM {
+ @Override
+ public Pivot create() {
+ return new Pivot(AXIS_Y, PIVOT_MAX);
+ }
+ };
+
+ public abstract Pivot create();
+ }
+
+ @IntDef({AXIS_X, AXIS_Y})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Axis{
+ }
+}
+
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java
new file mode 100644
index 0000000000..74a0d26087
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java
@@ -0,0 +1,84 @@
+package com.yarolegovich.discretescrollview.transform;
+
+import android.view.View;
+import androidx.annotation.FloatRange;
+
+/**
+ * Created by yarolegovich on 03.03.2017.
+ */
+public class ScaleTransformer implements DiscreteScrollItemTransformer {
+
+ private Pivot pivotX;
+ private Pivot pivotY;
+ private float minScale;
+ private float maxMinDiff;
+
+ public ScaleTransformer() {
+ pivotX = Pivot.X.CENTER.create();
+ pivotY = Pivot.Y.CENTER.create();
+ minScale = 0.8f;
+ maxMinDiff = 0.2f;
+ }
+
+ @Override
+ public void transformItem(View item, float position) {
+ pivotX.setOn(item);
+ pivotY.setOn(item);
+ float closenessToCenter = 1f - Math.abs(position);
+ float scale = minScale + maxMinDiff * closenessToCenter;
+ item.setScaleX(scale);
+ item.setScaleY(scale);
+ }
+
+ public static class Builder {
+
+ private ScaleTransformer transformer;
+ private float maxScale;
+
+ public Builder() {
+ transformer = new ScaleTransformer();
+ maxScale = 1f;
+ }
+
+ public Builder setMinScale(@FloatRange(from = 0.01) float scale) {
+ transformer.minScale = scale;
+ return this;
+ }
+
+ public Builder setMaxScale(@FloatRange(from = 0.01) float scale) {
+ maxScale = scale;
+ return this;
+ }
+
+ public Builder setPivotX(Pivot.X pivotX) {
+ return setPivotX(pivotX.create());
+ }
+
+ public Builder setPivotX(Pivot pivot) {
+ assertAxis(pivot, Pivot.AXIS_X);
+ transformer.pivotX = pivot;
+ return this;
+ }
+
+ public Builder setPivotY(Pivot.Y pivotY) {
+ return setPivotY(pivotY.create());
+ }
+
+ public Builder setPivotY(Pivot pivot) {
+ assertAxis(pivot, Pivot.AXIS_Y);
+ transformer.pivotY = pivot;
+ return this;
+ }
+
+ public ScaleTransformer build() {
+ transformer.maxMinDiff = maxScale - transformer.minScale;
+ return transformer;
+ }
+
+ private void assertAxis(Pivot pivot, @Pivot.Axis int axis) {
+ if (pivot.getAxis() != axis) {
+ throw new IllegalArgumentException("You passed a Pivot for wrong axis.");
+ }
+ }
+ }
+}
diff --git a/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java
new file mode 100644
index 0000000000..265ab331f0
--- /dev/null
+++ b/libraries/card-library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java
@@ -0,0 +1,44 @@
+package com.yarolegovich.discretescrollview.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import com.yarolegovich.discretescrollview.DiscreteScrollView;
+
+/**
+ * Created by yarolegovich on 16.03.2017.
+ */
+public class ScrollListenerAdapter implements DiscreteScrollView.ScrollStateChangeListener {
+
+ private DiscreteScrollView.ScrollListener adaptee;
+
+ public ScrollListenerAdapter(@NonNull DiscreteScrollView.ScrollListener adaptee) {
+ this.adaptee = adaptee;
+ }
+
+ @Override
+ public void onScrollStart(@NonNull T currentItemHolder, int adapterPosition) {
+
+ }
+
+ @Override
+ public void onScrollEnd(@NonNull T currentItemHolder, int adapterPosition) {
+
+ }
+
+ @Override
+ public void onScroll(float scrollPosition,
+ int currentIndex, int newIndex,
+ @Nullable T currentHolder, @Nullable T newCurrentHolder) {
+ adaptee.onScroll(scrollPosition, currentIndex, newIndex, currentHolder, newCurrentHolder);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ScrollListenerAdapter) {
+ return adaptee.equals(((ScrollListenerAdapter) obj).adaptee);
+ } else {
+ return super.equals(obj);
+ }
+ }
+}
diff --git a/libraries/card-library/src/main/res/values/attr.xml b/libraries/card-library/src/main/res/values/attr.xml
new file mode 100644
index 0000000000..7310089318
--- /dev/null
+++ b/libraries/card-library/src/main/res/values/attr.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libraries/card-library/src/main/res/values/strings.xml b/libraries/card-library/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..4909bfbff7
--- /dev/null
+++ b/libraries/card-library/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ You should not set LayoutManager on DiscreteScrollView.class instance. Library uses a special one. Just don\'t call the method.
+ InfiniteScrollAdapter is supposed to work only with DiscreteScrollView
+
diff --git a/modules/mogo-module-apps/build.gradle b/modules/mogo-module-apps/build.gradle
index 8b87ab74eb..4ac0696a0d 100644
--- a/modules/mogo-module-apps/build.gradle
+++ b/modules/mogo-module-apps/build.gradle
@@ -43,10 +43,10 @@ dependencies {
implementation rootProject.ext.dependencies.material
annotationProcessor rootProject.ext.dependencies.aroutercompiler
implementation rootProject.ext.dependencies.androidxrecyclerview
-
if (Boolean.valueOf(RELEASE)) {
implementation rootProject.ext.dependencies.mogomap
implementation rootProject.ext.dependencies.mogomapapi
+ implementation rootProject.ext.dependencies.mogomapapi
implementation rootProject.ext.dependencies.mogoutils
api rootProject.ext.dependencies.mogocommons
api rootProject.ext.dependencies.mogoserviceapi
@@ -58,6 +58,8 @@ dependencies {
api project(":foudations:mogo-commons")
api project(':services:mogo-service-api')
implementation project(':modules:mogo-module-common')
+ implementation project(":libraries:card-library")
+
}
}
diff --git a/modules/mogo-module-apps/src/main/java/com/mogo/module/apps/AppNavigatorFragment.java b/modules/mogo-module-apps/src/main/java/com/mogo/module/apps/AppNavigatorFragment.java
index 5813143c2b..6cf4635b34 100644
--- a/modules/mogo-module-apps/src/main/java/com/mogo/module/apps/AppNavigatorFragment.java
+++ b/modules/mogo-module-apps/src/main/java/com/mogo/module/apps/AppNavigatorFragment.java
@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.alibaba.android.arouter.launcher.ARouter;
import com.mogo.commons.mvp.MvpFragment;
import com.mogo.module.apps.adapter.AppIndicatorAdapter;
+import com.mogo.module.apps.utils.CardScaleTransformer;
import com.mogo.module.apps.utils.LaunchUtils;
import com.mogo.module.common.MogoModulePaths;
import com.mogo.service.MogoServicePaths;
@@ -23,6 +24,8 @@ import com.mogo.service.fragmentmanager.IMogoFragmentManager;
import com.mogo.service.module.IMogoModuleProvider;
import com.mogo.utils.TipToast;
+import com.yarolegovich.discretescrollview.DiscreteScrollView;
+import com.yarolegovich.discretescrollview.transform.ScaleTransformer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -34,7 +37,8 @@ import java.util.Map;
* 描述
*/
public class AppNavigatorFragment extends MvpFragment
- implements AppNavigatorView {
+ implements AppNavigatorView, DiscreteScrollView.OnItemChangedListener,
+ DiscreteScrollView.ScrollStateChangeListener {
private View mApps;
@@ -46,7 +50,7 @@ public class AppNavigatorFragment extends MvpFragment integers = new ArrayList<>(10);
- integers.add(R.drawable.module_apps_ic_navigation);
+ integers.add(R.drawable.module_apps_ic_interest);
+ integers.add(R.drawable.module_apps_ic_online_car);
+ integers.add(R.drawable.module_apps_ic_news);
+ integers.add(R.drawable.module_apps_ic_tanlu);
integers.add(R.drawable.module_apps_ic_media_center);
- integers.add(R.drawable.module_apps_ic_car_settings);
- integers.add(R.drawable.module_apps_ic_car_settings);
- integers.add(R.drawable.module_apps_ic_car_settings);
AppIndicatorAdapter appIndicatorAdapter = new AppIndicatorAdapter(getContext(), integers);
scroller.setAdapter(appIndicatorAdapter);
- LinearSnapHelper snapHelper = new LinearSnapHelper();
- snapHelper.attachToRecyclerView(scroller);
-
- scroller.scrollToPosition(Integer.MAX_VALUE / 2);
+ scroller.scrollToPosition(Integer.MAX_VALUE / 2-1);
//mNavigation.setOnClickListener( view -> {
// openSearchPanel();
// trackNavigatorClickEvent( 1 );
@@ -163,4 +172,23 @@ public class AppNavigatorFragment extends MvpFragment {
@Override public void onBindViewHolder(RecycleViewHolder holder, Integer integer) {
ImageView ivIndicator = holder.getView(R.id.module_apps_id_app_icon);
ivIndicator.setImageResource(integer);
+ holder.setText(R.id.module_apps_id_app_name,names[holder.getLayoutPosition()%5] );
}
+ private String[] names=new String[]{"新鲜事","在线车辆","首页","探路","车聊聊"};
}
diff --git a/modules/mogo-module-apps/src/main/java/com/mogo/module/apps/utils/CardScaleTransformer.java b/modules/mogo-module-apps/src/main/java/com/mogo/module/apps/utils/CardScaleTransformer.java
new file mode 100644
index 0000000000..1bc8021d5d
--- /dev/null
+++ b/modules/mogo-module-apps/src/main/java/com/mogo/module/apps/utils/CardScaleTransformer.java
@@ -0,0 +1,91 @@
+package com.mogo.module.apps.utils;
+
+import android.view.View;
+import androidx.annotation.FloatRange;
+import com.mogo.module.apps.R;
+import com.yarolegovich.discretescrollview.transform.DiscreteScrollItemTransformer;
+import com.yarolegovich.discretescrollview.transform.Pivot;
+import com.yarolegovich.discretescrollview.transform.ScaleTransformer;
+
+/**
+ * @author zyz
+ * 2020-03-11.
+ */
+public class CardScaleTransformer implements DiscreteScrollItemTransformer {
+
+ private Pivot pivotX;
+ private Pivot pivotY;
+ private float minScale;
+ private float maxMinDiff;
+
+ public CardScaleTransformer() {
+ pivotX = Pivot.X.CENTER.create();
+ pivotY = Pivot.Y.BOTTOM.create();
+ minScale = 0.8f;
+ maxMinDiff = 0.2f;
+ }
+
+ @Override
+ public void transformItem(View item, float position) {
+
+ item= item.findViewById(R.id.module_apps_id_app_icon);
+ pivotX.setOn(item);
+ pivotY.setOn(item);
+ float closenessToCenter = 1f - Math.abs(position);
+ float scale = minScale + maxMinDiff * closenessToCenter;
+ item.setScaleX(scale);
+ item.setScaleY(scale);
+ }
+
+ public static class Builder {
+
+ private CardScaleTransformer transformer;
+ private float maxScale;
+
+ public Builder() {
+ transformer = new CardScaleTransformer();
+ maxScale = 1f;
+ }
+
+ public Builder setMinScale(@FloatRange(from = 0.01) float scale) {
+ transformer.minScale = scale;
+ return this;
+ }
+
+ public Builder setMaxScale(@FloatRange(from = 0.01) float scale) {
+ maxScale = scale;
+ return this;
+ }
+
+ public Builder setPivotX(Pivot.X pivotX) {
+ return setPivotX(pivotX.create());
+ }
+
+ public Builder setPivotX(Pivot pivot) {
+ assertAxis(pivot, Pivot.AXIS_X);
+ transformer.pivotX = pivot;
+ return this;
+ }
+
+ public Builder setPivotY(Pivot.Y pivotY) {
+ return setPivotY(pivotY.create());
+ }
+
+ public Builder setPivotY(Pivot pivot) {
+ assertAxis(pivot, Pivot.AXIS_Y);
+ transformer.pivotY = pivot;
+ return this;
+ }
+
+ public CardScaleTransformer build() {
+ transformer.maxMinDiff = maxScale - transformer.minScale;
+ return transformer;
+ }
+
+ private void assertAxis(Pivot pivot, @Pivot.Axis int axis) {
+ if (pivot.getAxis() != axis) {
+ throw new IllegalArgumentException("You passed a Pivot for wrong axis.");
+ }
+ }
+ }
+}
diff --git a/modules/mogo-module-apps/src/main/res/drawable-ldpi/module_apps_ic_apps.png b/modules/mogo-module-apps/src/main/res/drawable-ldpi/module_apps_ic_apps.png
deleted file mode 100755
index c564b8867f..0000000000
Binary files a/modules/mogo-module-apps/src/main/res/drawable-ldpi/module_apps_ic_apps.png and /dev/null differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_apps.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_apps.png
index 80ecfda0bd..f1fc6671f2 100755
Binary files a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_apps.png and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_apps.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_chat_icon.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_chat_icon.png
new file mode 100755
index 0000000000..9d74b24ea8
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_chat_icon.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_chat_unchecked.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_chat_unchecked.png
new file mode 100755
index 0000000000..9b4d5501dc
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_chat_unchecked.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_interest.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_interest.png
new file mode 100755
index 0000000000..5d0d4e1a6d
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_interest.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_interest_unchecked.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_interest_unchecked.png
new file mode 100755
index 0000000000..807d603b7b
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_interest_unchecked.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_media_center.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_media_center.png
old mode 100644
new mode 100755
index 13aa66438a..1e6247d47a
Binary files a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_media_center.png and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_media_center.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_media_center_checked.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_media_center_checked.png
new file mode 100755
index 0000000000..ded21b6af2
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_media_center_checked.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_news.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_news.png
new file mode 100755
index 0000000000..b3bc19464e
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_news.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_news_unchecked.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_news_unchecked.png
new file mode 100755
index 0000000000..5a64a68660
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_news_unchecked.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_online_car.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_online_car.png
new file mode 100755
index 0000000000..761a11b72a
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_online_car.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_online_car_unchecked.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_online_car_unchecked.png
new file mode 100755
index 0000000000..b967fc7071
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_online_car_unchecked.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_tanlu.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_tanlu.png
new file mode 100755
index 0000000000..edd6eaca26
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_tanlu.png differ
diff --git a/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_tanlu_unchecked.png b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_tanlu_unchecked.png
new file mode 100755
index 0000000000..ac2a05136b
Binary files /dev/null and b/modules/mogo-module-apps/src/main/res/drawable-xhdpi/module_apps_ic_tanlu_unchecked.png differ
diff --git a/modules/mogo-module-apps/src/main/res/layout/module_apps_fragment_apps_navigator.xml b/modules/mogo-module-apps/src/main/res/layout/module_apps_fragment_apps_navigator.xml
index 2a44855ab2..5c05b17a94 100644
--- a/modules/mogo-module-apps/src/main/res/layout/module_apps_fragment_apps_navigator.xml
+++ b/modules/mogo-module-apps/src/main/res/layout/module_apps_fragment_apps_navigator.xml
@@ -1,5 +1,5 @@
-
-
-
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/modules/mogo-module-apps/src/main/res/layout/module_apps_item_app_indicator.xml b/modules/mogo-module-apps/src/main/res/layout/module_apps_item_app_indicator.xml
index 8914c979ae..8e02e01c0e 100644
--- a/modules/mogo-module-apps/src/main/res/layout/module_apps_item_app_indicator.xml
+++ b/modules/mogo-module-apps/src/main/res/layout/module_apps_item_app_indicator.xml
@@ -1,26 +1,24 @@
+ android:layout_height="@dimen/dp_112" />
+ android:textSize="@dimen/dp_26" />
\ No newline at end of file
diff --git a/modules/mogo-module-apps/src/main/res/values-xhdpi/dimens.xml b/modules/mogo-module-apps/src/main/res/values-xhdpi/dimens.xml
index ff19f10f5d..8981b061bc 100644
--- a/modules/mogo-module-apps/src/main/res/values-xhdpi/dimens.xml
+++ b/modules/mogo-module-apps/src/main/res/values-xhdpi/dimens.xml
@@ -5,9 +5,9 @@
30px
4px
103px
- 120px
- 120px
- 60px
+ 94px
+ 94px
+ 43px
60px
32px
32px
diff --git a/modules/mogo-module-apps/src/main/res/values/dimens.xml b/modules/mogo-module-apps/src/main/res/values/dimens.xml
index 604c52ddec..f3a1d89594 100644
--- a/modules/mogo-module-apps/src/main/res/values/dimens.xml
+++ b/modules/mogo-module-apps/src/main/res/values/dimens.xml
@@ -14,4 +14,5 @@
220px
220px
154px
+ 174px
\ No newline at end of file
diff --git a/modules/mogo-module-main/src/main/java/com/mogo/module/main/MainActivity.java b/modules/mogo-module-main/src/main/java/com/mogo/module/main/MainActivity.java
index 101bfcf81d..8e5e7b14fd 100644
--- a/modules/mogo-module-main/src/main/java/com/mogo/module/main/MainActivity.java
+++ b/modules/mogo-module-main/src/main/java/com/mogo/module/main/MainActivity.java
@@ -69,7 +69,7 @@ public class MainActivity extends MvpActivity< MainView, MainPresenter > impleme
private IMogoStatusManager mMogoStatusManager;
private OrientedViewPager mCardsContainer;
- private VerticalStackTransformer mTransformer;
+ //private VerticalStackTransformer mTransformer;
private CardModulesAdapter mCardModulesAdapter;
private View mHeader;
@@ -101,7 +101,7 @@ public class MainActivity extends MvpActivity< MainView, MainPresenter > impleme
protected void initViews() {
mCardsContainer = findViewById( R.id.module_main_id_cards_container );
mCardsContainer.setOrientation( OrientedViewPager.Orientation.HORIZONTAL );
- mTransformer = new VerticalStackTransformer( this );
+ //mTransformer = new VerticalStackTransformer( this );
mCardsContainer.setOnPageChangeListener( mOnPageChangeListener = new OnPageChangeListenerAdapter() {
private boolean mIsLast = true;
private boolean mCardFlipStatus = false;
@@ -134,7 +134,7 @@ public class MainActivity extends MvpActivity< MainView, MainPresenter > impleme
}
} else if ( state == ViewPager.SCROLL_STATE_IDLE ) {
mCardFlipStatus = false;
- mTransformer.resetOffsetScroll();
+ //mTransformer.resetOffsetScroll();
}
int cardSize = mCardModulesAdapter.getCount();
@@ -159,7 +159,7 @@ public class MainActivity extends MvpActivity< MainView, MainPresenter > impleme
int positionOffsetPixels ) {
super.onPageScrolled( position, positionOffset, positionOffsetPixels );
Logger.d( TAG, "pageScrolled : offset --- " + positionOffset );
- mTransformer.offsetScrollChanged( positionOffset );
+ //mTransformer.offsetScrollChanged( positionOffset );
}
} );
@@ -299,7 +299,7 @@ public class MainActivity extends MvpActivity< MainView, MainPresenter > impleme
List< IMogoModuleProvider > providers = mMogoModuleHandler.loadCardsModule();
mCardModulesAdapter = new CardModulesAdapter( this, providers );
mCardsContainer.setOffscreenPageLimit( providers.size() );
- mCardsContainer.setPageTransformer( true, mTransformer );
+ //mCardsContainer.setPageTransformer( true, mTransformer );
mCardsContainer.setAdapter( mCardModulesAdapter );
mCardCoverUpBottomLayout.setVisibility( View.VISIBLE );
diff --git a/modules/mogo-module-main/src/main/res/drawable-xhdpi/module_apps_bg_card.png b/modules/mogo-module-main/src/main/res/drawable-xhdpi/module_apps_bg_card.png
new file mode 100755
index 0000000000..bb0c8aef1c
Binary files /dev/null and b/modules/mogo-module-main/src/main/res/drawable-xhdpi/module_apps_bg_card.png differ
diff --git a/modules/mogo-module-main/src/main/res/layout/module_main_activity_main.xml b/modules/mogo-module-main/src/main/res/layout/module_main_activity_main.xml
index 7173faacc9..c03383b847 100644
--- a/modules/mogo-module-main/src/main/res/layout/module_main_activity_main.xml
+++ b/modules/mogo-module-main/src/main/res/layout/module_main_activity_main.xml
@@ -37,14 +37,18 @@
android:layout_marginLeft="@dimen/module_main_card_container_marginLeft"
android:layout_marginTop="@dimen/module_main_card_container_marginTop">
+
+ android:overScrollMode="never" />
+ />
diff --git a/settings.gradle b/settings.gradle
index 4747a1bb37..b813673c90 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -4,6 +4,7 @@ include ':foudations:mogo-utils'
include ':services:mogo-service-api'
include ':services:mogo-service'
include ':libraries:mogo-map'
+include ':libraries:card-library'
include ':foudations:mogo-commons'
include ':modules:mogo-module-map'
include ':modules:mogo-module-common'