From 36245e162d188701e02441e372e1b5c6abda6996 Mon Sep 17 00:00:00 2001 From: Jonathan Caryl Date: Wed, 12 Apr 2017 09:28:21 +0100 Subject: [PATCH 01/61] Update gradle / wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 68 +++++---- gradlew.bat | 174 +++++++++++------------ 4 files changed, 126 insertions(+), 120 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..16e75d40ee5d4e8b16694f504ba632338e5b5a15 100644 GIT binary patch delta 24984 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 BeFgvk delta 24409 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 g@%w%UXe%pPd*|9 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282..e95643d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 641573ab20f0a55199c2fd1538843cf180e594c0 Mon Sep 17 00:00:00 2001 From: Jonathan Caryl Date: Wed, 12 Apr 2017 09:23:24 +0100 Subject: [PATCH 02/61] Extract versions to root project --- build.gradle | 8 +++++++- library/build.gradle | 10 +++++----- sample/build.gradle | 12 ++++++------ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index f16ba00..f8fda0c 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:2.3.1' classpath 'com.novoda:bintray-release:0.4.0' } } @@ -25,4 +25,10 @@ ext { description = 'Scrollable list of items, where current item is centered and can be changed using swipes.' publishVersion = '1.1.3' licences = ['Apache-2.0'] + + compileSdkVersion = 25 + buildToolsVersion = '25.0.2' + targetSdkVersion = compileSdkVersion + + supportLibVersion = '25.3.1' } \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index 524a226..45ad896 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -2,20 +2,20 @@ apply plugin: 'com.android.library' apply plugin: 'com.novoda.bintray-release' android { - compileSdkVersion 25 - buildToolsVersion "25.0.2" + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { minSdkVersion 14 - targetSdkVersion 25 + targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" } } dependencies { - compile 'com.android.support:appcompat-v7:25.2.0' - compile 'com.android.support:recyclerview-v7:25.2.0' + compile "com.android.support:appcompat-v7:$supportLibVersion" + compile "com.android.support:recyclerview-v7:$supportLibVersion" } publish { diff --git a/sample/build.gradle b/sample/build.gradle index 91d0758..a8711c8 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 25 - buildToolsVersion "25.0.2" + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { applicationId "com.yarolegovich.discretescrollview.sample" minSdkVersion 19 - targetSdkVersion 25 + targetSdkVersion rootProject.ext.targetSdkVersion versionCode 3 versionName "1.0" } @@ -22,9 +22,9 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.2.0' - compile 'com.android.support:cardview-v7:25.2.0' - compile 'com.android.support:design:25.2.0' + compile "com.android.support:appcompat-v7:$supportLibVersion" + compile "com.android.support:cardview-v7:$supportLibVersion" + compile "com.android.support:design:$supportLibVersion" compile 'com.github.bumptech.glide:glide:3.7.0' From d48977bf5be48540d6ef7a081a4ef22370cdd17b Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Fri, 21 Apr 2017 16:58:40 +0300 Subject: [PATCH 03/61] fixes issue #17, the bug in a scale transformer --- README.md | 2 +- build.gradle | 2 +- .../discretescrollview/transform/ScaleTransformer.java | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e074fde..f044b05 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.1.3' +compile 'com.yarolegovich:discrete-scrollview:1.1.4' ``` ## Sample Get it on Google Play
diff --git a/build.gradle b/build.gradle index f16ba00..87c8161 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,6 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'Scrollable list of items, where current item is centered and can be changed using swipes.' - publishVersion = '1.1.3' + publishVersion = '1.1.4' licences = ['Apache-2.0'] } \ No newline at end of file diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java b/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java index 8ec8d4f..18af2bd 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java @@ -12,15 +12,13 @@ public class ScaleTransformer implements DiscreteScrollItemTransformer { private Pivot pivotX; private Pivot pivotY; private float minScale; - private float maxScale; private float maxMinDiff; public ScaleTransformer() { pivotX = Pivot.X.CENTER.create(); pivotY = Pivot.Y.CENTER.create(); minScale = 0.8f; - maxScale = 1f; - maxMinDiff = maxScale - minScale; + maxMinDiff = 0.2f; } @Override @@ -36,9 +34,11 @@ public void transformItem(View item, float position) { 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) { @@ -47,7 +47,7 @@ public Builder setMinScale(@FloatRange(from = 0.01) float scale) { } public Builder setMaxScale(@FloatRange(from = 0.01) float scale) { - transformer.maxMinDiff = scale; + maxScale = scale; return this; } @@ -72,7 +72,7 @@ public Builder setPivotY(Pivot pivot) { } public ScaleTransformer build() { - transformer.maxMinDiff = transformer.maxScale - transformer.minScale; + transformer.maxMinDiff = maxScale - transformer.minScale; return transformer; } From f87115f1a1d4f573bc3cd15c7258f6586dea54c0 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 13 May 2017 19:25:20 +0300 Subject: [PATCH 04/61] API-breaking changes! now a list of listeners is maintained. also listeners are now notified when current item changed because of data set changes and on each notifyDataSetChanged call by adapter --- .../DiscreteScrollView.java | 144 ++++++++++++------ 1 file changed, 97 insertions(+), 47 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index 209bbb1..d694039 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -7,23 +7,29 @@ import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; +import android.util.Log; import android.view.View; 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 = Orientation.HORIZONTAL.ordinal(); private DiscreteScrollLayoutManager layoutManager; - private ScrollStateChangeListener scrollStateChangeListener; - private OnItemChangedListener onItemChangedListener; + private List scrollStateChangeListeners; + private List onItemChangedListeners; public DiscreteScrollView(Context context) { super(context); @@ -41,6 +47,9 @@ public DiscreteScrollView(Context context, AttributeSet attrs, int defStyleAttr) } 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); @@ -104,16 +113,61 @@ public void setOffscreenItems(int items) { layoutManager.setOffscreenItems(items); } - public void setScrollStateChangeListener(ScrollStateChangeListener scrollStateChangeListener) { - this.scrollStateChangeListener = scrollStateChangeListener; + 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); + } } - public void setScrollListener(ScrollListener scrollListener) { - setScrollStateChangeListener(new ScrollListenerAdapter(scrollListener)); + private void notifyScrollEnd(ViewHolder holder, int current) { + for (ScrollStateChangeListener listener : scrollStateChangeListeners) { + listener.onScrollEnd(holder, current); + } } - public void setOnItemChangedListener(OnItemChangedListener onItemChangedListener) { - this.onItemChangedListener = onItemChangedListener; + private void notifyScroll(float position, ViewHolder currentHolder, ViewHolder newHolder) { + for (ScrollStateChangeListener listener : scrollStateChangeListeners) { + listener.onScroll(position, 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 { @@ -125,62 +179,58 @@ public void onIsBoundReachedFlagChange(boolean isBoundReached) { @Override public void onScrollStart() { - if (scrollStateChangeListener != null) { - int current = layoutManager.getCurrentPosition(); - ViewHolder holder = getViewHolder(current); - if (holder != null) { - scrollStateChangeListener.onScrollStart(holder, current); - } + if (scrollStateChangeListeners.isEmpty()) { + return; + } + int current = layoutManager.getCurrentPosition(); + ViewHolder holder = getViewHolder(current); + if (holder != null) { + notifyScrollStart(holder, current); } } @Override public void onScrollEnd() { - ViewHolder holder = null; - int current = layoutManager.getCurrentPosition(); - if (scrollStateChangeListener != null) { - holder = getViewHolder(current); - if (holder == null) { - return; - } - scrollStateChangeListener.onScrollEnd(holder, current); + if (onItemChangedListeners.isEmpty() && scrollStateChangeListeners.isEmpty()) { + return; } - if (onItemChangedListener != null) { - if (holder == null) { - holder = getViewHolder(current); - } - if (holder != null) { - onItemChangedListener.onCurrentItemChanged(holder, current); - } + int current = layoutManager.getCurrentPosition(); + ViewHolder holder = getViewHolder(current); + if (holder != null) { + notifyScrollEnd(holder, current); + notifyCurrentItemChanged(holder, current); } } @Override public void onScroll(float currentViewPosition) { - if (scrollStateChangeListener != null) { - int current = getCurrentItem(); - ViewHolder currentHolder = getViewHolder(getCurrentItem()); + if (scrollStateChangeListeners.isEmpty()) { + return; + } + int current = getCurrentItem(); + ViewHolder currentHolder = getViewHolder(getCurrentItem()); - int newCurrent = current + (currentViewPosition < 0 ? 1 : -1); - ViewHolder newCurrentHolder = getViewHolder(newCurrent); + int newCurrent = current + (currentViewPosition < 0 ? 1 : -1); + ViewHolder newCurrentHolder = getViewHolder(newCurrent); - if (currentHolder != null && newCurrentHolder != null) { - scrollStateChangeListener.onScroll( - currentViewPosition, currentHolder, - newCurrentHolder); - } + if (currentHolder != null && newCurrentHolder != null) { + notifyScroll(currentViewPosition, currentHolder, newCurrentHolder); } } @Override public void onCurrentViewFirstLayout() { - if (onItemChangedListener != null) { - int current = layoutManager.getCurrentPosition(); - ViewHolder currentHolder = getViewHolder(current); - if (currentHolder != null) { - onItemChangedListener.onCurrentItemChanged(currentHolder, current); + post(new Runnable() { + @Override + public void run() { + notifyCurrentItemChanged(); } - } + }); + } + + @Override + public void onDataSetChangeChangedPosition() { + notifyCurrentItemChanged(); } } @@ -201,8 +251,8 @@ public interface ScrollListener { 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 -1 */ - void onCurrentItemChanged(@NonNull T viewHolder, int adapterPosition); - + void onCurrentItemChanged(@Nullable T viewHolder, int adapterPosition); } } From de12d03f181a9e85b0355b491d85edbfdc106014 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 13 May 2017 19:27:03 +0300 Subject: [PATCH 05/61] added predictive scroll handing and notifications about position changes caused by data set changes. moved current view first layout call to onLayoutComplete --- .../discretescrollview/Direction.java | 12 ++++ .../DiscreteScrollLayoutManager.java | 65 +++++++++++++++---- .../DiscreteScrollView.java | 1 - 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/Direction.java b/library/src/main/java/com/yarolegovich/discretescrollview/Direction.java index 8617e6f..e4d1c38 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/Direction.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/Direction.java @@ -10,16 +10,28 @@ enum Direction { 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/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 738fc65..b623de4 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -11,6 +11,7 @@ import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v7.widget.LinearSmoothScroller; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; @@ -23,9 +24,9 @@ */ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { - private static final String EXTRA_POSITION = "extra_position"; + static final int NO_POSITION = -1; - private static final int NO_POSITION = -1; + private static final String EXTRA_POSITION = "extra_position"; private static final int DEFAULT_TIME_FOR_ITEM_SETTLE = 150; //This field will take value of all visible view's center points during the fill phase @@ -53,12 +54,15 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { private SparseArray detachedCache; + private boolean dataSetChangeShiftedPosition; + private boolean isFirstOrEmptyLayout; + @NonNull private final ScrollStateListener scrollStateListener; private DiscreteScrollItemTransformer itemTransformer; public DiscreteScrollLayoutManager( - Context c, + @NonNull Context c, @NonNull ScrollStateListener scrollStateListener, @NonNull Orientation orientation) { this.context = c; @@ -83,9 +87,13 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State return; } - boolean isFirstOrEmptyLayout = getChildCount() == 0; - if (isFirstOrEmptyLayout) { - initChildDimensions(recycler); + //onLayoutChildren may be called multiple times and this check is required so that the flag + //won't be cleared until onLayoutCompleted + if (!isFirstOrEmptyLayout) { + isFirstOrEmptyLayout = getChildCount() == 0; + if (isFirstOrEmptyLayout) { + initChildDimensions(recycler); + } } updateRecyclerDimensions(); @@ -95,9 +103,16 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State fill(recycler); applyItemTransformToChildren(); + } + @Override + public void onLayoutCompleted(RecyclerView.State state) { if (isFirstOrEmptyLayout) { scrollStateListener.onCurrentViewFirstLayout(); + isFirstOrEmptyLayout = false; + } else if (dataSetChangeShiftedPosition) { + scrollStateListener.onDataSetChangeChangedPosition(); + dataSetChangeShiftedPosition = false; } } @@ -149,11 +164,19 @@ private void fill(RecyclerView.Recycler recycler) { private void layoutViews(RecyclerView.Recycler recycler, Direction direction, int endBound) { final int positionStep = direction.applyTo(1); + boolean noPredictiveLayoutRequired = pendingPosition == NO_POSITION + || !direction.sameAs(pendingPosition - currentPosition); + viewCenterIterator.set(currentViewCenter.x, currentViewCenter.y); - for (int i = currentPosition + positionStep; isInBounds(i); i += positionStep) { + 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, i, viewCenterIterator); + layoutView(recycler, pos, viewCenterIterator); + } else if (noPredictiveLayoutRequired) { + break; } } } @@ -195,26 +218,42 @@ private void recycleViewsAndClearCache(RecyclerView.Recycler recycler) { @Override public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { + int newPosition = currentPosition; if (currentPosition == NO_POSITION) { - currentPosition = 0; + newPosition = 0; } else if (currentPosition >= positionStart) { - currentPosition = Math.min(currentPosition + itemCount, getItemCount() - 1); + newPosition = Math.min(currentPosition + itemCount, getItemCount() - 1); } + onNewPosition(newPosition); } @Override public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { + int newPosition = currentPosition; if (getItemCount() == 0) { - currentPosition = NO_POSITION; + newPosition = NO_POSITION; } else if (currentPosition >= positionStart) { - currentPosition = Math.max(0, currentPosition - itemCount); + 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), getItemCount() - 1); + dataSetChangeShiftedPosition = true; + } + + private void onNewPosition(int position) { + if (currentPosition != position) { + currentPosition = position; + dataSetChangeShiftedPosition = true; + } } @Override @@ -575,6 +614,8 @@ public interface ScrollStateListener { void onScroll(float currentViewPosition); void onCurrentViewFirstLayout(); + + void onDataSetChangeChangedPosition(); } } diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index d694039..b199651 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -7,7 +7,6 @@ import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import com.yarolegovich.discretescrollview.transform.DiscreteScrollItemTransformer; From 5aa1a47aee741c582a1286eb5a4a4bacd18937cc Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 13 May 2017 19:29:57 +0300 Subject: [PATCH 06/61] infinite scroll adapter --- .../InfiniteScrollAdapter.java | 160 ++++++++++++++++++ .../transform/ScaleTransformer.java | 3 +- .../util/ScrollListenerAdapter.java | 11 +- library/src/main/res/values/strings.xml | 1 + 4 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java new file mode 100644 index 0000000..51e0d51 --- /dev/null +++ b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java @@ -0,0 +1,160 @@ +package com.yarolegovich.discretescrollview; + +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.ViewGroup; + +/** + * Created by yarolegovich on 28-Apr-17. + */ + +public class InfiniteScrollAdapter extends RecyclerView.Adapter { + + private static final int NOT_INITIALIZED = -1; + private static final int RESET_BOUND = 100; + + public static InfiniteScrollAdapter wrap(RecyclerView.Adapter adapter) { + return new InfiniteScrollAdapter<>(adapter); + } + + private RecyclerView.Adapter wrapped; + private DiscreteScrollLayoutManager layoutManager; + + private int currentRangeStart; + + public InfiniteScrollAdapter(RecyclerView.Adapter wrapped) { + this.wrapped = wrapped; + this.wrapped.registerAdapterDataObserver(new DataSetChangeDelegate()); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + wrapped.onAttachedToRecyclerView(recyclerView); + if (recyclerView instanceof DiscreteScrollView) { + layoutManager = (DiscreteScrollLayoutManager) recyclerView.getLayoutManager(); + currentRangeStart = NOT_INITIALIZED; + } else { + String msg = recyclerView.getContext().getString(R.string.dsv_ex_msg_adapter_wrong_recycler); + throw new RuntimeException(msg); + } + } + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + wrapped.onDetachedFromRecyclerView(recyclerView); + layoutManager = null; + } + + @Override + public T onCreateViewHolder(ViewGroup parent, int viewType) { + if (currentRangeStart == NOT_INITIALIZED) { + resetRange(0); + } + return wrapped.onCreateViewHolder(parent, viewType); + } + + @Override + public void onBindViewHolder(T holder, int position) { + wrapped.onBindViewHolder(holder, mapPositionToReal(position)); + } + + @Override + public int getItemViewType(int position) { + return wrapped.getItemViewType(mapPositionToReal(position)); + } + + @Override + public int getItemCount() { + return wrapped.getItemCount() == 0 ? 0 : Integer.MAX_VALUE; + } + + 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) { + int adapterTarget = currentRangeStart + position; + int adapterCurrent = layoutManager.getCurrentPosition(); + if (adapterTarget == adapterCurrent) { + return adapterCurrent; + } else if (adapterTarget < adapterCurrent) { + int adapterTargetNextSet = currentRangeStart + wrapped.getItemCount() + position; + return adapterCurrent - adapterTarget < adapterTargetNextSet - adapterCurrent ? + adapterTarget : adapterTargetNextSet; + } else { + int adapterTargetPrevSet = currentRangeStart - wrapped.getItemCount() + position; + return adapterCurrent - adapterTargetPrevSet < adapterTarget - adapterCurrent ? + adapterTargetPrevSet : adapterTarget; + } + } + + private int mapPositionToReal(int position) { + int newPosition = position - currentRangeStart; + if (newPosition >= wrapped.getItemCount()) { + currentRangeStart += wrapped.getItemCount(); + if (Integer.MAX_VALUE - currentRangeStart <= RESET_BOUND) { + resetRange(0); + } + return 0; + } else if (newPosition < 0) { + currentRangeStart -= wrapped.getItemCount(); + if (currentRangeStart <= RESET_BOUND) { + resetRange(wrapped.getItemCount() - 1); + } + return wrapped.getItemCount() - 1; + } else { + return newPosition; + } + } + + private void resetRange(int newPosition) { + currentRangeStart = Integer.MAX_VALUE / 2; + layoutManager.scrollToPosition(currentRangeStart + newPosition); + } + + //TODO: handle proper data set change notifications + private class DataSetChangeDelegate extends RecyclerView.AdapterDataObserver { + + @Override + public void onChanged() { + resetRange(0); + notifyDataSetChanged(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + onChanged(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + onChanged(); + } + + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java b/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java index 18af2bd..a42c313 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java @@ -6,8 +6,7 @@ /** * Created by yarolegovich on 03.03.2017. */ - -public class ScaleTransformer implements DiscreteScrollItemTransformer { +public class ScaleTransformer implements DiscreteScrollItemTransformer { private Pivot pivotX; private Pivot pivotY; diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java b/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java index 5de8190..af16b56 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java @@ -12,7 +12,7 @@ public class ScrollListenerAdapter implements private DiscreteScrollView.ScrollListener adaptee; - public ScrollListenerAdapter(DiscreteScrollView.ScrollListener adaptee) { + public ScrollListenerAdapter(@NonNull DiscreteScrollView.ScrollListener adaptee) { this.adaptee = adaptee; } @@ -30,4 +30,13 @@ public void onScrollEnd(@NonNull T currentItemHolder, int adapterPosition) { public void onScroll(float scrollPosition, @NonNull T currentHolder, @NonNull T newCurrentHolder) { adaptee.onScroll(scrollPosition, 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/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml index fb84655..4909bfb 100644 --- a/library/src/main/res/values/strings.xml +++ b/library/src/main/res/values/strings.xml @@ -1,3 +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 From d3c908cd2c7477869bdecefc53c70cf2a8dc10ff Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 13 May 2017 19:41:07 +0300 Subject: [PATCH 07/61] code style changes --- .../DiscreteScrollLayoutManager.java | 1 + .../discretescrollview/InfiniteScrollAdapter.java | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index b623de4..8ae0e4d 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -164,6 +164,7 @@ private void fill(RecyclerView.Recycler 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); diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java index 51e0d51..e54dab2 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java @@ -1,8 +1,7 @@ package com.yarolegovich.discretescrollview; -import android.support.annotation.Nullable; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.view.ViewGroup; /** @@ -14,7 +13,8 @@ public class InfiniteScrollAdapter extends Re private static final int NOT_INITIALIZED = -1; private static final int RESET_BOUND = 100; - public static InfiniteScrollAdapter wrap(RecyclerView.Adapter adapter) { + public static InfiniteScrollAdapter wrap( + @NonNull RecyclerView.Adapter adapter) { return new InfiniteScrollAdapter<>(adapter); } @@ -23,7 +23,7 @@ public static InfiniteScrollAdapter wrap( private int currentRangeStart; - public InfiniteScrollAdapter(RecyclerView.Adapter wrapped) { + public InfiniteScrollAdapter(@NonNull RecyclerView.Adapter wrapped) { this.wrapped = wrapped; this.wrapped.registerAdapterDataObserver(new DataSetChangeDelegate()); } @@ -154,7 +154,5 @@ public void onItemRangeInserted(int positionStart, int itemCount) { public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { onChanged(); } - } - } \ No newline at end of file From 438a61f25fd059811e66407cbf2b25b7ba8548bc Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 13 May 2017 19:48:17 +0300 Subject: [PATCH 08/61] refactored sample according to the new API --- .../discretescrollview/DiscreteScrollView.java | 2 +- .../sample/gallery/GalleryActivity.java | 12 ++++++++---- .../sample/shop/ShopActivity.java | 4 ++-- .../sample/weather/WeatherActivity.java | 13 ++++++++----- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index b199651..4aab937 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -250,7 +250,7 @@ public interface ScrollListener { 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 -1 + * 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/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java index c74fce8..1fab0b0 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java @@ -3,6 +3,7 @@ import android.animation.ArgbEvaluator; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; @@ -35,8 +36,8 @@ protected void onCreate(Bundle savedInstanceState) { List data = gallery.getData(); DiscreteScrollView itemPicker = (DiscreteScrollView) findViewById(R.id.item_picker); itemPicker.setAdapter(new GalleryAdapter(data)); - itemPicker.setScrollListener(this); - itemPicker.setOnItemChangedListener(this); + itemPicker.addScrollListener(this); + itemPicker.addOnItemChangedListener(this); itemPicker.scrollToPosition(1); findViewById(R.id.home).setOnClickListener(this); @@ -66,8 +67,11 @@ public void onScroll( } @Override - public void onCurrentItemChanged(@NonNull GalleryAdapter.ViewHolder viewHolder, int adapterPosition) { - viewHolder.setOverlayColor(currentOverlayColor); + public void onCurrentItemChanged(@Nullable GalleryAdapter.ViewHolder viewHolder, int adapterPosition) { + //viewHolder will never be null, because we never remove items from adapter's list + if (viewHolder != null) { + viewHolder.setOverlayColor(currentOverlayColor); + } } private void share(View view) { diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java index f0d95ba..67db938 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java @@ -47,7 +47,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { data = shop.getData(); itemPicker = (DiscreteScrollView) findViewById(R.id.item_picker); itemPicker.setOrientation(Orientation.HORIZONTAL); - itemPicker.setOnItemChangedListener(this); + itemPicker.addOnItemChangedListener(this); itemPicker.setAdapter(new ShopAdapter(data)); itemPicker.setItemTransitionTimeMillis(DiscreteScrollViewOptions.getTransitionTime()); itemPicker.setItemTransformer(new ScaleTransformer.Builder() @@ -105,7 +105,7 @@ private void changeRateButtonState(Item item) { } @Override - public void onCurrentItemChanged(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + public void onCurrentItemChanged(@Nullable RecyclerView.ViewHolder viewHolder, int position) { onItemChanged(data.get(position)); } diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java index c0a8320..e998ebd 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java @@ -37,8 +37,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { forecasts = WeatherStation.get().getForecasts(); cityPicker = (DiscreteScrollView) findViewById(R.id.forecast_city_picker); cityPicker.setAdapter(new ForecastAdapter(forecasts)); - cityPicker.setOnItemChangedListener(this); - cityPicker.setScrollStateChangeListener(this); + cityPicker.addOnItemChangedListener(this); + cityPicker.addScrollStateChangeListener(this); cityPicker.scrollToPosition(2); cityPicker.setItemTransitionTimeMillis(DiscreteScrollViewOptions.getTransitionTime()); cityPicker.setItemTransformer(new ScaleTransformer.Builder() @@ -53,9 +53,12 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } @Override - public void onCurrentItemChanged(@NonNull ForecastAdapter.ViewHolder holder, int position) { - forecastView.setForecast(forecasts.get(position)); - holder.showText(); + public void onCurrentItemChanged(@Nullable ForecastAdapter.ViewHolder holder, int position) { + //viewHolder will never be null, because we never remove items from adapter's list + if (holder != null) { + forecastView.setForecast(forecasts.get(position)); + holder.showText(); + } } @Override From b1202aa04a1f5aa53376ff7265a61180807a7646 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 13 May 2017 20:14:06 +0300 Subject: [PATCH 09/61] build file changes --- build.gradle | 2 +- sample/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f6046ae..bb0e833 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'Scrollable list of items, where current item is centered and can be changed using swipes.' - publishVersion = '1.1.4' + publishVersion = '1.2.0-beta' licences = ['Apache-2.0'] compileSdkVersion = 25 diff --git a/sample/build.gradle b/sample/build.gradle index a8711c8..9704d96 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -7,7 +7,7 @@ android { applicationId "com.yarolegovich.discretescrollview.sample" minSdkVersion 19 targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 3 + versionCode 4 versionName "1.0" } From 007518250f239401fbd7aace0dd3a6afce0f7e6b Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 13 May 2017 20:54:22 +0300 Subject: [PATCH 10/61] back to previous gradle version to fix build errors, added infinite scroll to one of the examples from sample, changed README. closes issue #7 --- README.md | 40 ++++++++++++++++--- build.gradle | 4 +- .../sample/DiscreteScrollViewOptions.java | 11 ++++- .../sample/shop/ShopActivity.java | 8 +++- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f044b05..7ae2989 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.1.4' +compile 'com.yarolegovich:discrete-scrollview:1.2.0' ``` + ## Sample Get it on Google Play
@@ -83,10 +84,34 @@ cityPicker.setItemTransformer(new ScaleTransformer.Builder() ``` You may see how it works on GIFs. +#### Infinite scroll +Infinite scroll is implemented on the adapter level: +```java +InfiniteScrollAdapter wrapper = InfiniteScrollAdapter.wrap(yourAdapter); +scrollView.setAdapter(wrapper); +``` +An instance of `InfiniteScrollAdapter` has the following useful methods: +```java +int getRealItemCount(); + +int getRealCurrentPosition(); + +int getRealPosition(int position); + +/* + * You will probably want this method in the following use case: + * int targetAdapterPosition = wrapper.getClosestPosition(targetPosition); + * scrollView.smoothScrollTo(targetAdapterPosition); + * To scroll the data set for the least required amount to reach targetPosition. + */ +int getClosestPosition(int position); +``` +Currently `InfiniteScrollAdapter` handles data set changes inefficiently, so your contributions are welcome. #### Callbacks * Scroll state changes: ```java -scrollView.setScrollStateChangeListener(listener); +scrollView.addScrollStateChangeListener(listener); +scrollView.removeScrollStateChangeListener(listener); public interface ScrollStateChangeListener { @@ -109,7 +134,8 @@ public interface ScrollStateChangeListener { ``` * Scroll: ```java -scrollView.setScrollListener(listener); +scrollView.addScrollListener(listener); +scrollView.removeScrollListener(listener); public interface ScrollListener { //The same as ScrollStateChangeListener, but for the cases when you are interested only in onScroll() @@ -118,14 +144,16 @@ public interface ScrollListener { ``` * Current selection changes: ```java -scrollView.setOnItemChangedListener(listener); +scrollView.addOnItemChangedListener(listener); +scrollView.removeOnItemChangedListener(listener); public interface OnItemChangedListener { /** * Called when new item is selected. It is similar to the onScrollEnd of ScrollStateChangeListener, except that it is * also called when currently selected item appears on the screen for the first time. + * viewHolder will be null, if data set becomes empty */ - void onCurrentItemChanged(@NonNull T viewHolder, int adapterPosition); + void onCurrentItemChanged(@Nullable T viewHolder, int adapterPosition); } ``` @@ -147,4 +175,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -``` +``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index bb0e833..33daf3c 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.1' + classpath 'com.android.tools.build:gradle:2.2.3' classpath 'com.novoda:bintray-release:0.4.0' } } @@ -23,7 +23,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'Scrollable list of items, where current item is centered and can be changed using swipes.' - publishVersion = '1.2.0-beta' + publishVersion = '1.2.0' licences = ['Apache-2.0'] compileSdkVersion = 25 diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java index cee1be8..20c56a2 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java @@ -6,11 +6,13 @@ import android.support.design.widget.BottomSheetDialog; import android.support.v7.preference.PreferenceManager; import android.support.v7.widget.PopupMenu; +import android.support.v7.widget.RecyclerView; import android.view.Menu; import android.view.MenuItem; import android.view.View; import com.yarolegovich.discretescrollview.DiscreteScrollView; +import com.yarolegovich.discretescrollview.InfiniteScrollAdapter; import java.lang.ref.WeakReference; @@ -55,13 +57,20 @@ public void onClick(View v) { public static void smoothScrollToUserSelectedPosition(final DiscreteScrollView scrollView, View anchor) { PopupMenu popupMenu = new PopupMenu(scrollView.getContext(), anchor); Menu menu = popupMenu.getMenu(); - for (int i = 0; i < scrollView.getAdapter().getItemCount(); i++) { + final RecyclerView.Adapter adapter = scrollView.getAdapter(); + int itemCount = (adapter instanceof InfiniteScrollAdapter) ? + ((InfiniteScrollAdapter) adapter).getRealItemCount() : + adapter.getItemCount(); + for (int i = 0; i < itemCount; i++) { menu.add(String.valueOf(i + 1)); } popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { int destination = Integer.parseInt(String.valueOf(item.getTitle())) - 1; + if (adapter instanceof InfiniteScrollAdapter) { + destination = ((InfiniteScrollAdapter) adapter).getClosestPosition(destination); + } scrollView.smoothScrollToPosition(destination); return true; } diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java index 67db938..f5d8853 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java @@ -12,6 +12,7 @@ import android.widget.TextView; import com.yarolegovich.discretescrollview.DiscreteScrollView; +import com.yarolegovich.discretescrollview.InfiniteScrollAdapter; import com.yarolegovich.discretescrollview.Orientation; import com.yarolegovich.discretescrollview.sample.DiscreteScrollViewOptions; import com.yarolegovich.discretescrollview.sample.R; @@ -33,6 +34,7 @@ public class ShopActivity extends AppCompatActivity implements DiscreteScrollVie private TextView currentItemPrice; private ImageView rateItemButton; private DiscreteScrollView itemPicker; + private InfiniteScrollAdapter infiniteAdapter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -48,7 +50,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { itemPicker = (DiscreteScrollView) findViewById(R.id.item_picker); itemPicker.setOrientation(Orientation.HORIZONTAL); itemPicker.addOnItemChangedListener(this); - itemPicker.setAdapter(new ShopAdapter(data)); + infiniteAdapter = InfiniteScrollAdapter.wrap(new ShopAdapter(data)); + itemPicker.setAdapter(infiniteAdapter); itemPicker.setItemTransitionTimeMillis(DiscreteScrollViewOptions.getTransitionTime()); itemPicker.setItemTransformer(new ScaleTransformer.Builder() .setMinScale(0.8f) @@ -106,7 +109,8 @@ private void changeRateButtonState(Item item) { @Override public void onCurrentItemChanged(@Nullable RecyclerView.ViewHolder viewHolder, int position) { - onItemChanged(data.get(position)); + int positionInDataSet = infiniteAdapter.getRealPosition(position); + onItemChanged(data.get(positionInDataSet)); } private void showUnsupportedSnackBar() { From 34b23fc5be792e4105763563cf30ecf07514cb2b Mon Sep 17 00:00:00 2001 From: Srujan Barai Date: Thu, 22 Jun 2017 01:39:30 +0530 Subject: [PATCH 11/61] Added feature to allow sliding of multiple views on fling gesture. use 'picker.setSlideOnFling(true)' to use it. Custom threshold set to 2100 which can dynamically be set by user using 'picker.setSlideOnFlingThreshold(value)'. --- .../DiscreteScrollLayoutManager.java | 33 +++++++++++++++++-- .../DiscreteScrollView.java | 8 +++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 8ae0e4d..34cc88e 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -27,7 +27,7 @@ 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 = 150; + private static final int DEFAULT_TIME_FOR_ITEM_SETTLE = 300; //This field will take value of all visible view's center points during the fill phase private Point viewCenterIterator; @@ -57,6 +57,9 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { private boolean dataSetChangeShiftedPosition; private boolean isFirstOrEmptyLayout; + private int flingThreshold = 2100; //Decrease to increase sensitivity. + private boolean shouldSlideOnFling = false; + @NonNull private final ScrollStateListener scrollStateListener; private DiscreteScrollItemTransformer itemTransformer; @@ -183,6 +186,7 @@ private void layoutViews(RecyclerView.Recycler recycler, Direction direction, in } private void layoutView(RecyclerView.Recycler recycler, int position, Point viewCenter) { + if (position < 0) return; View v = detachedCache.get(position); if (v == null) { v = recycler.getViewForPosition(position); @@ -414,19 +418,32 @@ private void onDragStart() { public void onFling(int velocityX, int velocityY) { int velocity = orientationHelper.getFlingVelocity(velocityX, velocityY); - int newPosition = currentPosition + Direction.fromDelta(velocity).applyTo(1); + int throttleValue = shouldSlideOnFling ? Math.abs(velocityX/flingThreshold) : 1; + int newPosition = currentPosition + Direction.fromDelta(velocity).applyTo(throttleValue); + if (currentPosition != 0 && newPosition < 0) + newPosition = 0; + else if (currentPosition != getItemCount() -1 && newPosition >= getItemCount()) + newPosition = getItemCount() - 1; boolean isInScrollDirection = velocity * scrolled >= 0; boolean canFling = isInScrollDirection && newPosition >= 0 && newPosition < getItemCount(); if (canFling) { pendingScroll = getHowMuchIsLeftToScroll(velocity); if (pendingScroll != 0) { - startSmoothPendingScroll(); + startSmoothPendingScroll(newPosition); } } else { returnToCurrentPosition(); } } + public void setShouldSlideOnFling(Boolean result){ + this.shouldSlideOnFling = result; + } + + public void setSlideOnFlingThreshold(int threshold){ + this.flingThreshold = threshold; + } + public void returnToCurrentPosition() { pendingScroll = -scrolled; if (pendingScroll != 0) { @@ -465,6 +482,16 @@ private void startSmoothPendingScroll() { 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 void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { if (newAdapter.getItemCount() > 0) { diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index 4aab937..13623a6 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -104,6 +104,14 @@ 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(Orientation orientation) { layoutManager.setOrientation(orientation); } From 57751face88f3ffcb1dcb514839c402fea474e6e Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sun, 3 Sep 2017 10:08:43 +0700 Subject: [PATCH 12/61] Updated build tools and library versions --- build.gradle | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 33daf3c..fcd687b 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:2.3.3' classpath 'com.novoda:bintray-release:0.4.0' } } @@ -11,6 +11,7 @@ buildscript { allprojects { repositories { jcenter() + maven { url "https://maven.google.com" } } } @@ -26,9 +27,9 @@ ext { publishVersion = '1.2.0' licences = ['Apache-2.0'] - compileSdkVersion = 25 - buildToolsVersion = '25.0.2' - targetSdkVersion = compileSdkVersion + compileSdkVersion = 26 + buildToolsVersion = '26.0.1' + targetSdkVersion = 25 - supportLibVersion = '25.3.1' + supportLibVersion = '26.0.0' } \ No newline at end of file From ed8f1893c1c7fe6d617937330857283776190712 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sun, 3 Sep 2017 10:35:02 +0700 Subject: [PATCH 13/61] Code style changes --- .../DiscreteScrollLayoutManager.java | 43 ++++++++++++------- .../DiscreteScrollView.java | 2 +- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 34cc88e..ef1fae2 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -11,7 +11,6 @@ import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v7.widget.LinearSmoothScroller; import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; @@ -28,6 +27,7 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { 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. //This field will take value of all visible view's center points during the fill phase private Point viewCenterIterator; @@ -57,8 +57,8 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { private boolean dataSetChangeShiftedPosition; private boolean isFirstOrEmptyLayout; - private int flingThreshold = 2100; //Decrease to increase sensitivity. - private boolean shouldSlideOnFling = false; + private int flingThreshold; + private boolean shouldSlideOnFling; @NonNull private final ScrollStateListener scrollStateListener; @@ -72,6 +72,8 @@ public DiscreteScrollLayoutManager( 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(); @@ -418,12 +420,9 @@ private void onDragStart() { public void onFling(int velocityX, int velocityY) { int velocity = orientationHelper.getFlingVelocity(velocityX, velocityY); - int throttleValue = shouldSlideOnFling ? Math.abs(velocityX/flingThreshold) : 1; + int throttleValue = shouldSlideOnFling ? Math.abs(velocityX / flingThreshold) : 1; int newPosition = currentPosition + Direction.fromDelta(velocity).applyTo(throttleValue); - if (currentPosition != 0 && newPosition < 0) - newPosition = 0; - else if (currentPosition != getItemCount() -1 && newPosition >= getItemCount()) - newPosition = getItemCount() - 1; + newPosition = checkNewOnFlingPositionIsInBounds(newPosition); boolean isInScrollDirection = velocity * scrolled >= 0; boolean canFling = isInScrollDirection && newPosition >= 0 && newPosition < getItemCount(); if (canFling) { @@ -436,14 +435,6 @@ else if (currentPosition != getItemCount() -1 && newPosition >= getItemCount()) } } - public void setShouldSlideOnFling(Boolean result){ - this.shouldSlideOnFling = result; - } - - public void setSlideOnFlingThreshold(int threshold){ - this.flingThreshold = threshold; - } - public void returnToCurrentPosition() { pendingScroll = -scrolled; if (pendingScroll != 0) { @@ -545,6 +536,14 @@ public void setOrientation(Orientation orientation) { requestLayout(); } + public void setShouldSlideOnFling(boolean result){ + shouldSlideOnFling = result; + } + + public void setSlideOnFlingThreshold(int threshold){ + flingThreshold = threshold; + } + public int getCurrentPosition() { return currentPosition; } @@ -566,6 +565,18 @@ private float getCenterRelativePositionOf(View v) { return Math.min(Math.max(-1f, distanceFromCenter / scrollToChangeCurrent), 1f); } + private int checkNewOnFlingPositionIsInBounds(int position) { + //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 != getItemCount() - 1 && position >= getItemCount()) { + return getItemCount() - 1; + } + return position; + } + private int getHowMuchIsLeftToScroll(int dx) { return Direction.fromDelta(dx).applyTo(scrollToChangeCurrent - Math.abs(scrolled)); } diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index 13623a6..2b961be 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -104,7 +104,7 @@ public void setItemTransitionTimeMillis(@IntRange(from = 10) int millis) { layoutManager.setTimeForItemSettle(millis); } - public void setSlideOnFling(Boolean result){ + public void setSlideOnFling(boolean result){ layoutManager.setShouldSlideOnFling(result); } From 4710ac1fe60b01236c7c63f37a2843cc017c1826 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sun, 3 Sep 2017 10:38:43 +0700 Subject: [PATCH 14/61] Bug fix in sample app --- .../discretescrollview/sample/shop/ShopActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java index f5d8853..54c0770 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java @@ -72,7 +72,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { public void onClick(View v) { switch (v.getId()) { case R.id.item_btn_rate: - Item current = data.get(itemPicker.getCurrentItem()); + int realPosition = infiniteAdapter.getRealPosition(itemPicker.getCurrentItem()); + Item current = data.get(realPosition); shop.setRated(current.getId(), !shop.isRated(current.getId())); changeRateButtonState(current); break; From 8b5c3d286ae83e533cec1aa6264b48d2c6d2f0d5 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sun, 3 Sep 2017 11:32:10 +0700 Subject: [PATCH 15/61] Correct onScroll callback value for smoothScrollToPosition Unfortunately the change breaks public API a bit. Now when we are scrolling to a position through multiple positions onScroll callback receives correct newViewHolder and also two new parameters: currentIndex an d newIndex, this is required because non-adjacent ViewHolder may be null (when out of screen). closes #38 --- .../DiscreteScrollLayoutManager.java | 18 ++++++++-- .../DiscreteScrollView.java | 34 ++++++++++++------- .../util/ScrollListenerAdapter.java | 7 ++-- .../sample/gallery/GalleryActivity.java | 13 ++++--- .../sample/weather/WeatherActivity.java | 15 ++++---- 5 files changed, 58 insertions(+), 29 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index ef1fae2..9bed403 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -324,7 +324,7 @@ public void scrollToPosition(int position) { @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { - if (currentPosition == position) { + if (currentPosition == position || pendingPosition != NO_POSITION) { return; } @@ -428,6 +428,7 @@ public void onFling(int velocityX, int velocityY) { if (canFling) { pendingScroll = getHowMuchIsLeftToScroll(velocity); if (pendingScroll != 0) { + pendingPosition = newPosition; startSmoothPendingScroll(newPosition); } } else { @@ -516,6 +517,16 @@ public RecyclerView.LayoutParams generateDefaultLayoutParams() { 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; } @@ -598,7 +609,10 @@ public int getExtraLayoutSpace() { } private void notifyScroll() { - float position = -Math.min(Math.max(-1f, scrolled / (float) scrollToChangeCurrent), 1f); + float amountToScroll = pendingPosition != NO_POSITION ? + Math.abs(scrolled + pendingScroll) : + scrollToChangeCurrent; + float position = -Math.min(Math.max(-1f, scrolled / amountToScroll), 1f); scrollStateListener.onScroll(position); } diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index 2b961be..b2f09fe 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -156,9 +156,13 @@ private void notifyScrollEnd(ViewHolder holder, int current) { } } - private void notifyScroll(float position, ViewHolder currentHolder, ViewHolder newHolder) { + private void notifyScroll(float position, + int currentIndex, int newIndex, + ViewHolder currentHolder, ViewHolder newHolder) { for (ScrollStateChangeListener listener : scrollStateChangeListeners) { - listener.onScroll(position, currentHolder, newHolder); + listener.onScroll(position, currentIndex, newIndex, + currentHolder, + newHolder); } } @@ -214,15 +218,12 @@ public void onScroll(float currentViewPosition) { if (scrollStateChangeListeners.isEmpty()) { return; } - int current = getCurrentItem(); - ViewHolder currentHolder = getViewHolder(getCurrentItem()); - - int newCurrent = current + (currentViewPosition < 0 ? 1 : -1); - ViewHolder newCurrentHolder = getViewHolder(newCurrent); - - if (currentHolder != null && newCurrentHolder != null) { - notifyScroll(currentViewPosition, currentHolder, newCurrentHolder); - } + int currentIndex = getCurrentItem(); + int newIndex = layoutManager.getNextPosition(); + notifyScroll(currentViewPosition, + currentIndex, newIndex, + getViewHolder(currentIndex), + getViewHolder(newIndex)); } @Override @@ -247,12 +248,19 @@ public interface ScrollStateChangeListener { void onScrollEnd(@NonNull T currentItemHolder, int adapterPosition); - void onScroll(float scrollPosition, @NonNull T currentHolder, @NonNull T newCurrent); + void onScroll(float scrollPosition, + int currentPosition, + int newPosition, + @Nullable T currentHolder, + @Nullable T newCurrent); } public interface ScrollListener { - void onScroll(float scrollPosition, @NonNull T currentHolder, @NonNull T newCurrent); + void onScroll(float scrollPosition, + int currentPosition, int newPosition, + @Nullable T currentHolder, + @Nullable T newCurrent); } public interface OnItemChangedListener { diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java b/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java index af16b56..f47d7df 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java @@ -1,6 +1,7 @@ package com.yarolegovich.discretescrollview.util; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import com.yarolegovich.discretescrollview.DiscreteScrollView; @@ -27,8 +28,10 @@ public void onScrollEnd(@NonNull T currentItemHolder, int adapterPosition) { } @Override - public void onScroll(float scrollPosition, @NonNull T currentHolder, @NonNull T newCurrentHolder) { - adaptee.onScroll(scrollPosition, currentHolder, newCurrentHolder); + public void onScroll(float scrollPosition, + int currentIndex, int newIndex, + @Nullable T currentHolder, @Nullable T newCurrentHolder) { + adaptee.onScroll(scrollPosition, currentIndex, newIndex, currentHolder, newCurrentHolder); } @Override diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java index 1fab0b0..4decd15 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java @@ -59,11 +59,14 @@ public void onClick(View v) { @Override public void onScroll( float currentPosition, - @NonNull GalleryAdapter.ViewHolder currentHolder, - @NonNull GalleryAdapter.ViewHolder newCurrent) { - float position = Math.abs(currentPosition); - currentHolder.setOverlayColor(interpolate(position, currentOverlayColor, overlayColor)); - newCurrent.setOverlayColor(interpolate(position, overlayColor, currentOverlayColor)); + int currentIndex, int newIndex, + @Nullable GalleryAdapter.ViewHolder currentHolder, + @Nullable GalleryAdapter.ViewHolder newCurrent) { + if (currentHolder != null && newCurrent != null) { + float position = Math.abs(currentPosition); + currentHolder.setOverlayColor(interpolate(position, currentOverlayColor, overlayColor)); + newCurrent.setOverlayColor(interpolate(position, overlayColor, currentOverlayColor)); + } } @Override diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java index e998ebd..6f2cc4e 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java @@ -3,7 +3,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; +import android.support.v7.app.AppCompatActivity; import android.view.View; import com.yarolegovich.discretescrollview.DiscreteScrollView; @@ -36,6 +36,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { forecasts = WeatherStation.get().getForecasts(); cityPicker = (DiscreteScrollView) findViewById(R.id.forecast_city_picker); + cityPicker.setSlideOnFling(true); cityPicker.setAdapter(new ForecastAdapter(forecasts)); cityPicker.addOnItemChangedListener(this); cityPicker.addScrollStateChangeListener(this); @@ -69,12 +70,12 @@ public void onScrollStart(@NonNull ForecastAdapter.ViewHolder holder, int positi @Override public void onScroll( float position, - @NonNull ForecastAdapter.ViewHolder currentHolder, - @NonNull ForecastAdapter.ViewHolder newHolder) { - Forecast current = forecasts.get(cityPicker.getCurrentItem()); - int nextPosition = cityPicker.getCurrentItem() + (position > 0 ? -1 : 1); - if (nextPosition >= 0 && nextPosition < cityPicker.getAdapter().getItemCount()) { - Forecast next = forecasts.get(nextPosition); + int currentIndex, int newIndex, + @Nullable ForecastAdapter.ViewHolder currentHolder, + @Nullable ForecastAdapter.ViewHolder newHolder) { + Forecast current = forecasts.get(currentIndex); + if (newIndex >= 0 && newIndex < cityPicker.getAdapter().getItemCount()) { + Forecast next = forecasts.get(newIndex); forecastView.onScroll(1f - Math.abs(position), current, next); } } From a4f78130a8f6d5ac29241c3ddcdf5b2912775b59 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sun, 3 Sep 2017 12:06:48 +0700 Subject: [PATCH 16/61] Updated version number and README --- README.md | 21 +++++++++++++++++---- build.gradle | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7ae2989..98d8228 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.2.0' +compile 'com.yarolegovich:discrete-scrollview:1.3.0' ``` ## Sample @@ -84,6 +84,17 @@ cityPicker.setItemTransformer(new ScaleTransformer.Builder() ``` You may see how it works on GIFs. +#### Slide through multiple items + +To allow slide through multiple items call: +```java +scrollView.setSlideOnFling(true); +``` +The default threshold is set to 2100. Lower the threshold, more fluid the animation. You can adjust the threshold by calling: +```java +scrollView.setSlideOnFlingThreshold(value); +``` + #### Infinite scroll Infinite scroll is implemented on the adapter level: ```java @@ -126,10 +137,12 @@ public interface ScrollStateChangeListener { * -view1 is on position -1; * -currentlySelectedView is on position 0; * -view2 is on position 1. + * @param currentIndex - index of current view + * @param newIndex - index of a view which is becoming the new current * @param currentHolder - ViewHolder of a current view - * @param newCurrent - ViewHolder of a view that moved closer to the center + * @param newCurrent - ViewHolder of a view which is becoming the new current */ - void onScroll(float scrollPosition, @NonNull T currentHolder, @NonNull T newCurrentHolder); + void onScroll(float scrollPosition, int currentIndex, int newIndex, @Nullable T currentHolder, @Nullable T newCurrentHolder); } ``` * Scroll: @@ -139,7 +152,7 @@ scrollView.removeScrollListener(listener); public interface ScrollListener { //The same as ScrollStateChangeListener, but for the cases when you are interested only in onScroll() - void onScroll(float scrollPosition, @NonNull T currentHolder, @NonNull T newCurrentHolder); + void onScroll(float scrollPosition, int currentIndex, int newIndex, @Nullable T currentHolder, @Nullable T newCurrentHolder); } ``` * Current selection changes: diff --git a/build.gradle b/build.gradle index fcd687b..d285d1a 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'Scrollable list of items, where current item is centered and can be changed using swipes.' - publishVersion = '1.2.0' + publishVersion = '1.3.0' licences = ['Apache-2.0'] compileSdkVersion = 26 From bd07e7bf5e210d90aef5c240276b22de475ee490 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sun, 3 Sep 2017 12:21:26 +0700 Subject: [PATCH 17/61] minor refactoring --- .../DiscreteScrollLayoutManager.java | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 9bed403..0a9af18 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -327,14 +327,7 @@ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State if (currentPosition == position || pendingPosition != NO_POSITION) { return; } - - pendingScroll = -scrolled; - Direction direction = Direction.fromDelta(position - currentPosition); - int distanceToScroll = Math.abs(position - currentPosition) * scrollToChangeCurrent; - pendingScroll += direction.applyTo(distanceToScroll); - - pendingPosition = position; - startSmoothPendingScroll(); + startSmoothPendingScroll(position); } @Override @@ -424,13 +417,9 @@ public void onFling(int velocityX, int velocityY) { int newPosition = currentPosition + Direction.fromDelta(velocity).applyTo(throttleValue); newPosition = checkNewOnFlingPositionIsInBounds(newPosition); boolean isInScrollDirection = velocity * scrolled >= 0; - boolean canFling = isInScrollDirection && newPosition >= 0 && newPosition < getItemCount(); + boolean canFling = isInScrollDirection && isInBounds(newPosition); if (canFling) { - pendingScroll = getHowMuchIsLeftToScroll(velocity); - if (pendingScroll != 0) { - pendingPosition = newPosition; - startSmoothPendingScroll(newPosition); - } + startSmoothPendingScroll(newPosition); } else { returnToCurrentPosition(); } From cf1f0180e6c44aa11c703d036b808d4d2163d50d Mon Sep 17 00:00:00 2001 From: Boy Wang Date: Tue, 3 Oct 2017 19:15:49 +0800 Subject: [PATCH 18/61] Fix: Given null Adapter would raise NullPointerException. However, the internal layout states should always be reset when new Adapter is set. --- .../discretescrollview/DiscreteScrollLayoutManager.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 8ae0e4d..10b1696 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -467,11 +467,10 @@ private void startSmoothPendingScroll() { @Override public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { - if (newAdapter.getItemCount() > 0) { - pendingPosition = NO_POSITION; - scrolled = pendingScroll = 0; - currentPosition = 0; - } + pendingPosition = NO_POSITION; + scrolled = pendingScroll = 0; + currentPosition = 0; + removeAllViews(); } From 46fde41074aee864df830f0a2b7e4d2fcefe9869 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Tue, 24 Oct 2017 21:10:12 +0700 Subject: [PATCH 19/61] Fixed a bug introduced in #26 closes #48 --- .../discretescrollview/DiscreteScrollLayoutManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 0a9af18..253e34a 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -413,7 +413,7 @@ private void onDragStart() { public void onFling(int velocityX, int velocityY) { int velocity = orientationHelper.getFlingVelocity(velocityX, velocityY); - int throttleValue = shouldSlideOnFling ? Math.abs(velocityX / flingThreshold) : 1; + int throttleValue = shouldSlideOnFling ? Math.abs(velocity / flingThreshold) : 1; int newPosition = currentPosition + Direction.fromDelta(velocity).applyTo(throttleValue); newPosition = checkNewOnFlingPositionIsInBounds(newPosition); boolean isInScrollDirection = velocity * scrolled >= 0; From 6dd4f32229271262bcb1b95a25dbf14e0888dc63 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Tue, 24 Oct 2017 21:59:16 +0700 Subject: [PATCH 20/61] Version 1.3.1 The new version contains several bug fixes: * velocityX was used in calculations both for vertical and horizontal fling * null adapter caused NPE --- README.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98d8228..60f23b9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.3.0' +compile 'com.yarolegovich:discrete-scrollview:1.3.1' ``` ## Sample diff --git a/build.gradle b/build.gradle index d285d1a..23efd79 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'Scrollable list of items, where current item is centered and can be changed using swipes.' - publishVersion = '1.3.0' + publishVersion = '1.3.1' licences = ['Apache-2.0'] compileSdkVersion = 26 From 9dd50b6946cc2428341457ffb68a45b0dbe9105c Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 28 Oct 2017 19:06:26 +0200 Subject: [PATCH 21/61] Fixed a possibility of incorrect first layout Ensure that currentPosition is valid when onLayoutChildren is called and there items in the adapter. closes #44 --- .../discretescrollview/DiscreteScrollLayoutManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index dd52d98..5a1ed05 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -92,6 +92,10 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State return; } + if (currentPosition == NO_POSITION) { + currentPosition = 0; + } + //onLayoutChildren may be called multiple times and this check is required so that the flag //won't be cleared until onLayoutCompleted if (!isFirstOrEmptyLayout) { From 2d48c63a39ecafadcbb9675aa8ca1790aba10dff Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 28 Oct 2017 19:46:41 +0200 Subject: [PATCH 22/61] Fixed a bug with incorrectly triggered onScroll There was a possibility of onScroll being triggered incorrecly when user attempts to scroll to item before the first/after the last. --- .../discretescrollview/DiscreteScrollView.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index b2f09fe..540e44d 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -220,10 +220,12 @@ public void onScroll(float currentViewPosition) { } int currentIndex = getCurrentItem(); int newIndex = layoutManager.getNextPosition(); - notifyScroll(currentViewPosition, - currentIndex, newIndex, - getViewHolder(currentIndex), - getViewHolder(newIndex)); + if (currentIndex != newIndex) { + notifyScroll(currentViewPosition, + currentIndex, newIndex, + getViewHolder(currentIndex), + getViewHolder(newIndex)); + } } @Override From 14b94f22a193776cba2c34175cdc64f5b17fa229 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 28 Oct 2017 19:56:24 +0200 Subject: [PATCH 23/61] Updated library version --- README.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60f23b9..e644fa8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.3.1' +compile 'com.yarolegovich:discrete-scrollview:1.3.2' ``` ## Sample diff --git a/build.gradle b/build.gradle index 23efd79..ada7dd2 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'Scrollable list of items, where current item is centered and can be changed using swipes.' - publishVersion = '1.3.1' + publishVersion = '1.3.2' licences = ['Apache-2.0'] compileSdkVersion = 26 From bb7db105bd4f347e3e87c8b4b3546ea25465c5e1 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 28 Oct 2017 22:35:36 +0200 Subject: [PATCH 24/61] Prepared code for unit tests Extracted all method calls to recycler view like getWidth, getChildAt, recycleView etc. into a separate class RecyclerViewProxy to facilitate stubbing. Made methods and fields that are going to be used in testing protected. re #43 --- .../DiscreteScrollLayoutManager.java | 160 ++++++++------- .../discretescrollview/Orientation.java | 12 +- .../discretescrollview/RecyclerViewProxy.java | 105 ++++++++++ .../stub/StubRecyclerViewProxy.java | 194 ++++++++++++++++++ 4 files changed, 391 insertions(+), 80 deletions(-) create mode 100644 library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java create mode 100644 library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 5a1ed05..2fe9cb4 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -29,33 +29,36 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { private static final int DEFAULT_TIME_FOR_ITEM_SETTLE = 300; private static final int DEFAULT_FLING_THRESHOLD = 2100; //Decrease to increase sensitivity. + 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 - private Point viewCenterIterator; - private Point recyclerCenter; - private Point currentViewCenter; - private int childHalfWidth, childHalfHeight; - private int extraLayoutSpace; + 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 - private int scrollToChangeCurrent; - private int currentScrollState; + protected int scrollToChangeCurrent; + protected int currentScrollState; + + protected Orientation.Helper orientationHelper; + + protected int scrolled; + protected int pendingScroll; + protected int currentPosition; + protected int pendingPosition; - private Orientation.Helper orientationHelper; + protected SparseArray detachedCache; - private int scrolled; - private int pendingScroll; - private int currentPosition; - private int pendingPosition; + protected boolean isFirstOrEmptyLayout; private Context context; private int timeForItemSettle; private int offscreenItems; - private SparseArray detachedCache; - private boolean dataSetChangeShiftedPosition; - private boolean isFirstOrEmptyLayout; private int flingThreshold; private boolean shouldSlideOnFling; @@ -64,6 +67,8 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { private final ScrollStateListener scrollStateListener; private DiscreteScrollItemTransformer itemTransformer; + private RecyclerViewProxy recyclerViewProxy; + public DiscreteScrollLayoutManager( @NonNull Context c, @NonNull ScrollStateListener scrollStateListener, @@ -80,13 +85,14 @@ public DiscreteScrollLayoutManager( this.detachedCache = new SparseArray<>(); this.scrollStateListener = scrollStateListener; this.orientationHelper = orientation.createHelper(); + this.recyclerViewProxy = new RecyclerViewProxy(this); setAutoMeasureEnabled(true); } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (state.getItemCount() == 0) { - removeAndRecycleAllViews(recycler); + recyclerViewProxy.removeAndRecycleAllViews(recycler); currentPosition = pendingPosition = NO_POSITION; scrolled = pendingScroll = 0; return; @@ -99,7 +105,7 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.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 = getChildCount() == 0; + isFirstOrEmptyLayout = recyclerViewProxy.getChildCount() == 0; if (isFirstOrEmptyLayout) { initChildDimensions(recycler); } @@ -107,7 +113,7 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State updateRecyclerDimensions(); - detachAndScrapAttachedViews(recycler); + recyclerViewProxy.detachAndScrapAttachedViews(recycler); fill(recycler); @@ -125,13 +131,11 @@ public void onLayoutCompleted(RecyclerView.State state) { } } - private void initChildDimensions(RecyclerView.Recycler recycler) { - View viewToMeasure = recycler.getViewForPosition(0); - addView(viewToMeasure); - measureChildWithMargins(viewToMeasure, 0, 0); + protected void initChildDimensions(RecyclerView.Recycler recycler) { + View viewToMeasure = recyclerViewProxy.getMeasuredChildForAdapterPosition(0, recycler); - int childViewWidth = getDecoratedMeasuredWidth(viewToMeasure); - int childViewHeight = getDecoratedMeasuredHeight(viewToMeasure); + int childViewWidth = recyclerViewProxy.getMeasuredWidth(viewToMeasure); + int childViewHeight = recyclerViewProxy.getMeasuredHeight(viewToMeasure); childHalfWidth = childViewWidth / 2; childHalfHeight = childViewHeight / 2; @@ -142,19 +146,23 @@ private void initChildDimensions(RecyclerView.Recycler recycler) { extraLayoutSpace = scrollToChangeCurrent * offscreenItems; - detachAndScrapView(viewToMeasure, recycler); + recyclerViewProxy.detachAndScrapView(viewToMeasure, recycler); } - private void updateRecyclerDimensions() { - recyclerCenter.set(getWidth() / 2, getHeight() / 2); + protected void updateRecyclerDimensions() { + recyclerCenter.set( + recyclerViewProxy.getWidth() / 2, + recyclerViewProxy.getHeight() / 2); } - private void fill(RecyclerView.Recycler recycler) { + protected void fill(RecyclerView.Recycler recycler) { cacheAndDetachAttachedViews(); orientationHelper.setCurrentViewCenter(recyclerCenter, scrolled, currentViewCenter); - final int endBound = orientationHelper.getViewEnd(getWidth(), getHeight()); + final int endBound = orientationHelper.getViewEnd( + recyclerViewProxy.getWidth(), + recyclerViewProxy.getHeight()); //Layout current if (isViewVisible(currentViewCenter, endBound)) { @@ -167,7 +175,7 @@ private void fill(RecyclerView.Recycler recycler) { //Layout items after the current item layoutViews(recycler, Direction.END, endBound); - recycleViewsAndClearCache(recycler); + recycleDetachedViewsAndClearCache(recycler); } private void layoutViews(RecyclerView.Recycler recycler, Direction direction, int endBound) { @@ -191,38 +199,36 @@ private void layoutViews(RecyclerView.Recycler recycler, Direction direction, in } } - private void layoutView(RecyclerView.Recycler recycler, int position, Point viewCenter) { + protected void layoutView(RecyclerView.Recycler recycler, int position, Point viewCenter) { if (position < 0) return; View v = detachedCache.get(position); if (v == null) { - v = recycler.getViewForPosition(position); - addView(v); - measureChildWithMargins(v, 0, 0); - layoutDecoratedWithMargins(v, + v = recyclerViewProxy.getMeasuredChildForAdapterPosition(position, recycler); + recyclerViewProxy.layoutDecoratedWithMargins(v, viewCenter.x - childHalfWidth, viewCenter.y - childHalfHeight, viewCenter.x + childHalfWidth, viewCenter.y + childHalfHeight); } else { - attachView(v); + recyclerViewProxy.attachView(v); detachedCache.remove(position); } } - private void cacheAndDetachAttachedViews() { + protected void cacheAndDetachAttachedViews() { detachedCache.clear(); - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - detachedCache.put(getPosition(child), child); + 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++) { - detachView(detachedCache.valueAt(i)); + recyclerViewProxy.detachView(detachedCache.valueAt(i)); } } - private void recycleViewsAndClearCache(RecyclerView.Recycler recycler) { + protected void recycleDetachedViewsAndClearCache(RecyclerView.Recycler recycler) { for (int i = 0; i < detachedCache.size(); i++) { View viewToRemove = detachedCache.valueAt(i); - recycler.recycleView(viewToRemove); + recyclerViewProxy.recycleView(viewToRemove, recycler); } detachedCache.clear(); } @@ -233,7 +239,7 @@ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemC if (currentPosition == NO_POSITION) { newPosition = 0; } else if (currentPosition >= positionStart) { - newPosition = Math.min(currentPosition + itemCount, getItemCount() - 1); + newPosition = Math.min(currentPosition + itemCount, recyclerViewProxy.getItemCount() - 1); } onNewPosition(newPosition); } @@ -241,7 +247,7 @@ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemC @Override public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { int newPosition = currentPosition; - if (getItemCount() == 0) { + if (recyclerViewProxy.getItemCount() == 0) { newPosition = NO_POSITION; } else if (currentPosition >= positionStart) { if (currentPosition < positionStart + itemCount) { @@ -256,7 +262,7 @@ public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int ite @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), getItemCount() - 1); + currentPosition = Math.min(Math.max(0, currentPosition), recyclerViewProxy.getItemCount() - 1); dataSetChangeShiftedPosition = true; } @@ -277,8 +283,8 @@ public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerVi return scrollBy(dy, recycler); } - private int scrollBy(int amount, RecyclerView.Recycler recycler) { - if (getChildCount() == 0) { + protected int scrollBy(int amount, RecyclerView.Recycler recycler) { + if (recyclerViewProxy.getChildCount() == 0) { return 0; } @@ -294,7 +300,7 @@ private int scrollBy(int amount, RecyclerView.Recycler recycler) { pendingScroll -= delta; } - orientationHelper.offsetChildren(-delta, this); + orientationHelper.offsetChildren(-delta, recyclerViewProxy); if (orientationHelper.hasNewBecomeVisible(this)) { fill(recycler); @@ -307,10 +313,10 @@ private int scrollBy(int amount, RecyclerView.Recycler recycler) { return delta; } - private void applyItemTransformToChildren() { + protected void applyItemTransformToChildren() { if (itemTransformer != null) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); + for (int i = 0; i < recyclerViewProxy.getChildCount(); i++) { + View child = recyclerViewProxy.getChildAt(i); itemTransformer.transformItem(child, getCenterRelativePositionOf(child)); } } @@ -323,7 +329,7 @@ public void scrollToPosition(int position) { } currentPosition = position; - requestLayout(); + recyclerViewProxy.requestLayout(); } @Override @@ -436,7 +442,7 @@ public void returnToCurrentPosition() { } } - private int calculateAllowedScrollIn(Direction direction) { + protected int calculateAllowedScrollIn(Direction direction) { if (pendingScroll != 0) { return Math.abs(pendingScroll); } @@ -447,7 +453,7 @@ private int calculateAllowedScrollIn(Direction direction) { //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 == getItemCount() - 1) { + } 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); @@ -464,10 +470,10 @@ private int calculateAllowedScrollIn(Direction direction) { private void startSmoothPendingScroll() { LinearSmoothScroller scroller = new DiscreteLinearSmoothScroller(context); scroller.setTargetPosition(currentPosition); - startSmoothScroll(scroller); + recyclerViewProxy.startSmoothScroll(scroller); } - private void startSmoothPendingScroll(int position){ + private void startSmoothPendingScroll(int position) { if (currentPosition == position) return; pendingScroll = -scrolled; Direction direction = Direction.fromDelta(position - currentPosition); @@ -482,8 +488,7 @@ public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapt pendingPosition = NO_POSITION; scrolled = pendingScroll = 0; currentPosition = 0; - - removeAllViews(); + recyclerViewProxy.removeAllViews(); } @Override @@ -530,20 +535,20 @@ public void setTimeForItemSettle(int timeForItemSettle) { public void setOffscreenItems(int offscreenItems) { this.offscreenItems = offscreenItems; extraLayoutSpace = scrollToChangeCurrent * offscreenItems; - requestLayout(); + recyclerViewProxy.requestLayout(); } public void setOrientation(Orientation orientation) { orientationHelper = orientation.createHelper(); - removeAllViews(); - requestLayout(); + recyclerViewProxy.removeAllViews(); + recyclerViewProxy.requestLayout(); } - public void setShouldSlideOnFling(boolean result){ + public void setShouldSlideOnFling(boolean result) { shouldSlideOnFling = result; } - public void setSlideOnFlingThreshold(int threshold){ + public void setSlideOnFlingThreshold(int threshold) { flingThreshold = threshold; } @@ -554,7 +559,7 @@ public int getCurrentPosition() { @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); - if (getChildCount() > 0) { + if (recyclerViewProxy.getChildCount() > 0) { final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); record.setFromIndex(getPosition(getFirstChild())); record.setToIndex(getPosition(getLastChild())); @@ -569,13 +574,14 @@ private float getCenterRelativePositionOf(View v) { } 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 != getItemCount() - 1 && position >= getItemCount()) { - return getItemCount() - 1; + } else if (currentPosition != itemCount - 1 && position >= itemCount) { + return itemCount - 1; } return position; } @@ -585,15 +591,15 @@ private int getHowMuchIsLeftToScroll(int dx) { } private boolean isAnotherItemCloserThanCurrent() { - return Math.abs(scrolled) >= scrollToChangeCurrent * 0.6f; + return Math.abs(scrolled) >= scrollToChangeCurrent * SCROLL_TO_SNAP_TO_ANOTHER_ITEM; } public View getFirstChild() { - return getChildAt(0); + return recyclerViewProxy.getChildAt(0); } public View getLastChild() { - return getChildAt(getChildCount() - 1); + return recyclerViewProxy.getChildAt(recyclerViewProxy.getChildCount() - 1); } public int getExtraLayoutSpace() { @@ -602,14 +608,14 @@ public int getExtraLayoutSpace() { private void notifyScroll() { float amountToScroll = pendingPosition != NO_POSITION ? - Math.abs(scrolled + pendingScroll) : - scrollToChangeCurrent; + 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 < getItemCount(); + return itemPosition >= 0 && itemPosition < recyclerViewProxy.getItemCount(); } private boolean isViewVisible(Point viewCenter, int endBound) { @@ -618,6 +624,14 @@ private boolean isViewVisible(Point viewCenter, int endBound) { endBound, extraLayoutSpace); } + protected void setRecyclerViewProxy(RecyclerViewProxy recyclerViewProxy) { + this.recyclerViewProxy = recyclerViewProxy; + } + + protected void setOrientationHelper(Orientation.Helper orientationHelper) { + this.orientationHelper = orientationHelper; + } + private class DiscreteLinearSmoothScroller extends LinearSmoothScroller { public DiscreteLinearSmoothScroller(Context context) { diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java b/library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java index 9a5b3e8..22cb44f 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java @@ -1,7 +1,6 @@ package com.yarolegovich.discretescrollview; import android.graphics.Point; -import android.support.v7.widget.RecyclerView; import android.view.View; /** @@ -41,7 +40,7 @@ interface Helper { int getPendingDy(int pendingScroll); - void offsetChildren(int amount, RecyclerView.LayoutManager lm); + void offsetChildren(int amount, RecyclerViewProxy lm); float getDistanceFromCenter(Point center, int viewCenterX, int viewCenterY); @@ -100,8 +99,8 @@ public boolean hasNewBecomeVisible(DiscreteScrollLayoutManager lm) { } @Override - public void offsetChildren(int amount, RecyclerView.LayoutManager lm) { - lm.offsetChildrenHorizontal(amount); + public void offsetChildren(int amount, RecyclerViewProxy helper) { + helper.offsetChildrenHorizontal(amount); } @Override @@ -161,8 +160,8 @@ public void shiftViewCenter(Direction direction, int shiftAmount, Point outCente } @Override - public void offsetChildren(int amount, RecyclerView.LayoutManager lm) { - lm.offsetChildrenVertical(amount); + public void offsetChildren(int amount, RecyclerViewProxy helper) { + helper.offsetChildrenVertical(amount); } @Override @@ -217,5 +216,4 @@ public int getPendingDy(int pendingScroll) { } } - ; } diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java b/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java new file mode 100644 index 0000000..213c39d --- /dev/null +++ b/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java @@ -0,0 +1,105 @@ +package com.yarolegovich.discretescrollview; + +import android.support.annotation.NonNull; +import android.support.v7.widget.LinearSmoothScroller; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/** + * 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 getMeasuredWidth(View child) { + return layoutManager.getDecoratedMeasuredWidth(child); + } + + public int getMeasuredHeight(View child) { + return layoutManager.getDecoratedMeasuredHeight(child); + } + + 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/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java b/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java new file mode 100644 index 0000000..d4b5753 --- /dev/null +++ b/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java @@ -0,0 +1,194 @@ +package com.yarolegovich.discretescrollview.stub; + +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import com.yarolegovich.discretescrollview.RecyclerViewProxy; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.Mockito.mock; + +/** + * Created by yarolegovich on 10/28/17. + */ + +public class StubRecyclerViewProxy extends RecyclerViewProxy { + + private int width, height; + private int childWidth, childHeight; + private List children; + private int adapterItemCount; + + public StubRecyclerViewProxy(@NonNull RecyclerView.LayoutManager layoutManager) { + super(layoutManager); + children = new ArrayList<>(); + } + + @Override + public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) { + for (StubChildInfo childInfo : children) { + recycleView(childInfo.view, recycler); + } + removeAllViews(); + } + + @Override + public int getChildCount() { + return children.size(); + } + + @Override + public int getItemCount() { + return adapterItemCount; + } + + @Override + public View getMeasuredChildForAdapterPosition(int position, RecyclerView.Recycler recycler) { + if (position < adapterItemCount) { + return new StubChildInfo(0, position).view; + } + throw new IndexOutOfBoundsException(); + } + + @Override + public View getChildAt(int index) { + return children.get(index).view; + } + + @Override + public int getPosition(View view) { + for (StubChildInfo info : children) { + if (info.view == view) return info.adapterPosition; + } + throw new IllegalArgumentException(); + } + + @Override + public int getMeasuredWidth(View child) { + return childWidth; + } + + @Override + public int getMeasuredHeight(View child) { + return childHeight; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public void removeAllViews() { + children.clear(); + } + + @Override + public void offsetChildrenHorizontal(int amount) { + //NOP + } + + @Override + public void offsetChildrenVertical(int amount) { + //NOP + } + + @Override + public void attachView(View view) { + //NOP + } + + @Override + public void detachView(View view) { + //NOP + } + + @Override + public void detachAndScrapView(View view, RecyclerView.Recycler recycler) { + //NOP + } + + @Override + public void detachAndScrapAttachedViews(RecyclerView.Recycler recycler) { + //NOP + } + + @Override + public void recycleView(View view, RecyclerView.Recycler recycler) { + //NOP + } + + @Override + public void layoutDecoratedWithMargins(View v, int left, int top, int right, int bottom) { + //NOP + } + + @Override + public void requestLayout() { + //NOP + } + + @Override + public void startSmoothScroll(RecyclerView.SmoothScroller smoothScroller) { + //NOP + } + + public void addChildren(int childCount, int firstChildAdapterPosition) { + for (int i = 0; i < childCount; i++) { + children.add(new StubChildInfo(i, firstChildAdapterPosition + i)); + } + } + + public void setAdapterItemCount(int adapterItemCount) { + this.adapterItemCount = adapterItemCount; + } + + private static class StubChildInfo { + public final View view; + public final int recyclerChildIndex; + public final int adapterPosition; + + private StubChildInfo(int recyclerChildIndex, int adapterPosition) { + this.view = mock(View.class); + this.recyclerChildIndex = recyclerChildIndex; + this.adapterPosition = adapterPosition; + } + } + + public static class Builder { + StubRecyclerViewProxy target; + + public Builder(RecyclerView.LayoutManager lm) { + target = new StubRecyclerViewProxy(lm); + } + + public Builder withAdapterItemCount(int count) { + target.adapterItemCount = count; + return this; + } + + public Builder withRecyclerDimensions(int width, int height) { + target.width = width; + target.height = height; + return this; + } + + public Builder withChildDimensions(int width, int height) { + target.childWidth = width; + target.childHeight = height; + return this; + } + + public StubRecyclerViewProxy create() { + return target; + } + } +} From a7555b75a0f360ddad23c60d44b2143f6eb0c540 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 28 Oct 2017 22:37:59 +0200 Subject: [PATCH 25/61] Bunch of unit tests for both orientations of DiscreteScrollLayoutManager re #43 --- library/build.gradle | 5 + .../DiscreteScrollLayoutManagerTest.java | 553 ++++++++++++++++++ ...zontalDiscreteScrollLayoutManagerTest.java | 19 + ...rticalDiscreteScrollLayoutManagerTest.java | 19 + 4 files changed, 596 insertions(+) create mode 100644 library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java create mode 100644 library/src/test/java/com/yarolegovich/discretescrollview/HorizontalDiscreteScrollLayoutManagerTest.java create mode 100644 library/src/test/java/com/yarolegovich/discretescrollview/VerticalDiscreteScrollLayoutManagerTest.java diff --git a/library/build.gradle b/library/build.gradle index 45ad896..9f569df 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -16,6 +16,11 @@ android { dependencies { compile "com.android.support:appcompat-v7:$supportLibVersion" compile "com.android.support:recyclerview-v7:$supportLibVersion" + + testCompile 'org.robolectric:robolectric:3.0' + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.hamcrest:hamcrest-library:1.3' } publish { diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java b/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java new file mode 100644 index 0000000..7953072 --- /dev/null +++ b/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java @@ -0,0 +1,553 @@ +package com.yarolegovich.discretescrollview; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import com.yarolegovich.discretescrollview.stub.StubRecyclerViewProxy; + +import org.junit.Before; +import org.junit.Test; +import org.robolectric.RuntimeEnvironment; + +import java.util.Arrays; +import java.util.List; + +import static com.yarolegovich.discretescrollview.DiscreteScrollLayoutManager.NO_POSITION; +import static com.yarolegovich.discretescrollview.DiscreteScrollLayoutManager.SCROLL_TO_SNAP_TO_ANOTHER_ITEM; +import static org.mockito.Matchers.any; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Created by yarolegovich on 10/25/17. + */ +public abstract class DiscreteScrollLayoutManagerTest { + + private static final int RECYCLER_WIDTH = 400; + private static final int RECYCLER_HEIGHT = 600; + private static final int CHILD_WIDTH = 130; + private static final int CHILD_HEIGHT = 600; + private static final int ADAPTER_ITEM_COUNT = 10; + + private DiscreteScrollLayoutManager layoutManager; + private DiscreteScrollLayoutManager.ScrollStateListener mockScrollStateListener; + private StubRecyclerViewProxy stubRecyclerViewProxy; + private Orientation.Helper stubOrientationHelper; + private RecyclerView.State stubState; + + @Before + public void setUp() { + stubState = mock(RecyclerView.State.class); + stubOrientationHelper = spy(getOrientationToTest().createHelper()); + mockScrollStateListener = mock(DiscreteScrollLayoutManager.ScrollStateListener.class); + + layoutManager = spy(new DiscreteScrollLayoutManager( + RuntimeEnvironment.application, + mockScrollStateListener, + getOrientationToTest())); + + stubRecyclerViewProxy = spy(new StubRecyclerViewProxy.Builder(layoutManager) + .withRecyclerDimensions(RECYCLER_WIDTH, RECYCLER_HEIGHT) + .withChildDimensions(CHILD_WIDTH, CHILD_HEIGHT) + .withAdapterItemCount(ADAPTER_ITEM_COUNT) + .create()); + + layoutManager.setRecyclerViewProxy(stubRecyclerViewProxy); + layoutManager.setOrientationHelper(stubOrientationHelper); + } + + protected abstract Orientation getOrientationToTest(); + + @Test + public void onLayoutChildren_noItems_removesViewsAndResetsState() { + layoutManager.pendingScroll = 200; + layoutManager.scrolled = 1000; + layoutManager.pendingPosition = 1; + layoutManager.currentPosition = 2; + when(stubState.getItemCount()).thenReturn(0); + + layoutManager.onLayoutChildren(null, stubState); + + verify(stubRecyclerViewProxy).removeAndRecycleAllViews(any(RecyclerView.Recycler.class)); + assertThat(layoutManager.pendingScroll, is(0)); + assertThat(layoutManager.scrolled, is(0)); + assertThat(layoutManager.pendingPosition, is(NO_POSITION)); + assertThat(layoutManager.currentPosition, is(NO_POSITION)); + } + + @Test + public void onLayoutChildren_whenFirstOrEmptyLayout_childDimensionsAreInitialized() { + layoutManager.childHalfWidth = 0; + layoutManager.childHalfHeight = 0; + when(stubState.getItemCount()).thenReturn(ADAPTER_ITEM_COUNT); + + layoutManager.onLayoutChildren(null, stubState); + + assertThat(layoutManager.childHalfWidth, is(CHILD_WIDTH / 2)); + assertThat(layoutManager.childHalfHeight, is(CHILD_HEIGHT / 2)); + } + + @Test + public void onLayoutChildren_notFirstOrEmptyLayout_childDimensionsAreNotInitialized() { + stubRecyclerViewProxy.addChildren(5, 0); + + layoutManager.onLayoutChildren(null, stubState); + + assertThat(layoutManager.childHalfWidth, is(0)); + assertThat(layoutManager.childHalfHeight, is(0)); + } + + @Test + public void onLayoutChildren_multipleCallsInLayoutPhase_isFirstOrEmptyLayoutFlagNotCleared() { + when(stubState.getItemCount()).thenReturn(ADAPTER_ITEM_COUNT); + + layoutManager.onLayoutChildren(null, stubState); + stubRecyclerViewProxy.addChildren(5, 0); + layoutManager.onLayoutChildren(null, stubState); + + assertTrue(layoutManager.isFirstOrEmptyLayout); + } + + @Test + public void onLayoutCompleted_isFirstOrEmptyLayoutFlagSet_scrollStateListenerIsNotified() { + layoutManager.isFirstOrEmptyLayout = true; + + layoutManager.onLayoutCompleted(stubState); + + verify(mockScrollStateListener).onCurrentViewFirstLayout(); + } + + @Test + public void onLayoutCompleted_isFirstOrEmptyLayoutFlagSet_theFlagIsCleared() { + layoutManager.isFirstOrEmptyLayout = true; + + layoutManager.onLayoutCompleted(stubState); + + assertFalse(layoutManager.isFirstOrEmptyLayout); + } + + @Test + public void initChildDimensions_offscreenItemsNotSet_noExtraLayoutSpace() { + layoutManager.extraLayoutSpace = 1000; + + layoutManager.initChildDimensions(null); + + assertThat(layoutManager.extraLayoutSpace, is(0)); + } + + @Test + public void initChildDimensions_offscreenItemsSet_extraLayoutSpaceIsCalculated() { + layoutManager.extraLayoutSpace = 0; + layoutManager.setOffscreenItems(5); + + layoutManager.initChildDimensions(null); + + assertThat(layoutManager.extraLayoutSpace, is(not(0))); + } + + @Test + public void updateRecyclerDimensions_recyclerCenterIsInitialized() { + layoutManager.recyclerCenter.set(0, 0); + + layoutManager.updateRecyclerDimensions(); + + assertThat(layoutManager.recyclerCenter.x, is(RECYCLER_WIDTH / 2)); + assertThat(layoutManager.recyclerCenter.y, is(RECYCLER_HEIGHT / 2)); + } + + @Test + public void cacheAndDetachAttachedViews_allRecyclerChildrenArePutToCache() { + final int childCount = 6; + stubRecyclerViewProxy.addChildren(6, 0); + + layoutManager.cacheAndDetachAttachedViews(); + + assertThat(layoutManager.detachedCache.size(), is(childCount)); + for (int i = 0; i < childCount; i++) { + int position = stubRecyclerViewProxy.getPosition(stubRecyclerViewProxy.getChildAt(i)); + assertNotNull(layoutManager.detachedCache.get(position)); + } + } + + @Test + public void cacheAndDetachAttachedViews_allRecyclerChildrenAreDetached() { + final int childCount = 5; + stubRecyclerViewProxy.addChildren(childCount, 0); + + layoutManager.cacheAndDetachAttachedViews(); + + for (int i = 0; i < childCount; i++) { + verify(stubRecyclerViewProxy).detachView(stubRecyclerViewProxy.getChildAt(i)); + } + } + + @Test + public void recycleDetachedViewsAndClearCache_cacheIsClearedAndViewsAreRecycled() { + List views = Arrays.asList(mock(View.class), mock(View.class), mock(View.class)); + for (int i = 0; i < views.size(); i++) layoutManager.detachedCache.put(i, views.get(i)); + + layoutManager.recycleDetachedViewsAndClearCache(null); + + assertThat(layoutManager.detachedCache.size(), is(0)); + verify(stubRecyclerViewProxy, times(views.size())) + .recycleView(argThat(isIn(views)), any(RecyclerView.Recycler.class)); + } + + @Test + public void onItemsAdded_afterCurrentPosition_currentPositionIsUnchanged() { + final int addedItems = 3; + final int initialCurrent = ADAPTER_ITEM_COUNT / 2; + layoutManager.currentPosition = initialCurrent; + stubRecyclerViewProxy.setAdapterItemCount(ADAPTER_ITEM_COUNT + addedItems); + + layoutManager.onItemsAdded(null, initialCurrent + 1, addedItems); + + assertThat(layoutManager.currentPosition, is(initialCurrent)); + } + + @Test + public void onItemsAdded_beforeCurrentPosition_currentIsShiftedByAmountOfAddedItems() { + final int addedItems = 3; + final int initialCurrent = ADAPTER_ITEM_COUNT / 2; + layoutManager.currentPosition = initialCurrent; + stubRecyclerViewProxy.setAdapterItemCount(ADAPTER_ITEM_COUNT + addedItems); + + layoutManager.onItemsAdded(null, initialCurrent - 1, addedItems); + + assertThat(layoutManager.currentPosition, is(initialCurrent + addedItems)); + } + + @Test + public void onItemsRemoved_afterCurrentPosition_currentIsUnchanged() { + final int initialCurrent = ADAPTER_ITEM_COUNT / 2; + final int removedItems = initialCurrent / 2; + layoutManager.currentPosition = initialCurrent; + stubRecyclerViewProxy.setAdapterItemCount(ADAPTER_ITEM_COUNT - removedItems); + + layoutManager.onItemsRemoved(null, initialCurrent + 1, removedItems); + + assertThat(layoutManager.currentPosition, is(initialCurrent)); + } + + @Test + public void onItemsRemoved_beforeCurrentPosition_currentIsShiftedByAmountRemoved() { + final int initialCurrent = ADAPTER_ITEM_COUNT / 2; + final int removedItems = initialCurrent / 2; + layoutManager.currentPosition = initialCurrent; + stubRecyclerViewProxy.setAdapterItemCount(ADAPTER_ITEM_COUNT - removedItems); + + layoutManager.onItemsRemoved(null, 0, removedItems); + + assertThat(layoutManager.currentPosition, is(initialCurrent - removedItems)); + } + + @Test + public void onItemsRemoved_rangeWhichContainsCurrent_currentIsReset() { + final int initialCurrent = ADAPTER_ITEM_COUNT / 2; + layoutManager.currentPosition = initialCurrent; + + layoutManager.onItemsRemoved(null, initialCurrent - 1, 3); + + assertThat(layoutManager.currentPosition, is(0)); + } + + @Test + public void onItemsChanged_removedItemWhichWasCurrent_currentRemainsInValidRange() { + layoutManager.currentPosition = ADAPTER_ITEM_COUNT - 1; + stubRecyclerViewProxy.setAdapterItemCount(ADAPTER_ITEM_COUNT - 3); + + layoutManager.onItemsChanged(null); + + assertThat(layoutManager.currentPosition, is(stubRecyclerViewProxy.getItemCount() - 1)); + } + + @Test + public void scrollBy_noChildren_noScrollPerformed() { + doReturn(0).when(layoutManager).getChildCount(); + + int scrolled = layoutManager.scrollBy(1000, null); + + assertThat(scrolled, is(0)); + } + + @Test + public void scrollBy_moreScrollRequestedThanCanPerform_scrollsByAllAvailableAmount() { + final int requested = 1000, maxAvailable = 333; + prepareStubsForScrollBy(maxAvailable, 3, false); + + int scrolled = layoutManager.scrollBy(requested, null); + + assertThat(scrolled, both(not(equalTo(requested))).and(is(equalTo(maxAvailable)))); + } + + @Test + public void scrollBy_offsetsChildrenByNegativeScrollDelta() { + final int requested = 1000; + prepareStubsForScrollBy(requested, 3, false); + + int scrolled = layoutManager.scrollBy(requested, null); + + verify(stubOrientationHelper).offsetChildren(-scrolled, stubRecyclerViewProxy); + } + + @Test + public void scrollBy_noNewViewBecameVisible_fillIsNotCalled() { + prepareStubsForScrollBy(1000, 10, false); + + layoutManager.scrollBy(200, null); + + verify(layoutManager, never()).fill(any(RecyclerView.Recycler.class)); + } + + @Test + public void scrollBy_newViewBecomesVisible_fillIsCalled() { + final int childCount = 5; + stubRecyclerViewProxy.addChildren(childCount, 0); + prepareStubsForScrollBy(1000, childCount, true); + + layoutManager.scrollBy(200, null); + + verify(layoutManager).fill(any(RecyclerView.Recycler.class)); + } + + @Test + public void scrollBy_hasPendingScroll_pendingScrollDecreasedByScrolledAmount() { + final int initialPendingScroll = 1000; + layoutManager.pendingScroll = initialPendingScroll; + prepareStubsForScrollBy(300, 3, false); + + int scrolled = layoutManager.scrollBy(200, null); + + assertThat(layoutManager.pendingScroll, is(initialPendingScroll - scrolled)); + } + + @Test + public void scrollBy_scrollIsAccumulated() { + int initialScrolled = layoutManager.scrolled; + prepareStubsForScrollBy(300, 3, false); + + int scrolled = layoutManager.scrollBy(100, null); + + assertThat(layoutManager.scrolled, is(initialScrolled + scrolled)); + } + + @Test + public void scrollBy_scrolledMoreThanZero_listenerIsNotifiedAboutScroll() { + prepareStubsForScrollBy(300, 3, false); + + int scrolled = layoutManager.scrollBy(200, null); + + assertThat(scrolled, is(greaterThan(0))); + verify(mockScrollStateListener).onScroll(anyFloat()); + } + + @Test + public void scrollBy_scrolledByZero_listenerIsNotNotifiedAboutScroll() { + prepareStubsForScrollBy(0, 2, false); + + int scrolled = layoutManager.scrollBy(300, null); + + assertThat(scrolled, is(0)); + verify(mockScrollStateListener, never()).onScroll(anyFloat()); + } + + @Test + public void scrollBy_triesToScrollToTheItemBeforeFirst_onBoundReachedIsTrue() { + layoutManager.currentPosition = 0; + stubRecyclerViewProxy.addChildren(5, 0); + + layoutManager.scrollBy(-100, null); + + verify(mockScrollStateListener).onIsBoundReachedFlagChange(true); + } + + @Test + public void scrollBy_triesToScrollToTheItemAfterLast_onBoundReachedIsTrue() { + layoutManager.currentPosition = ADAPTER_ITEM_COUNT - 1; + stubRecyclerViewProxy.addChildren(5, ADAPTER_ITEM_COUNT - 5); + + layoutManager.scrollBy(100, null); + + verify(mockScrollStateListener).onIsBoundReachedFlagChange(true); + } + + @Test + public void scrollBy_scrollsToAllowedElement_onBoundReachedIsFalse() { + layoutManager.currentPosition = ADAPTER_ITEM_COUNT / 2; + stubRecyclerViewProxy.addChildren(5, 0); + + layoutManager.scrollBy(100, null); + + verify(mockScrollStateListener).onIsBoundReachedFlagChange(false); + } + + @Test + public void onScrollStateChanged_dragStartedWhenWasIdle_listenerNotifiedAboutScrollStart() { + layoutManager.currentScrollState = RecyclerView.SCROLL_STATE_IDLE; + + layoutManager.onScrollStateChanged(RecyclerView.SCROLL_STATE_DRAGGING); + + verify(mockScrollStateListener).onScrollStart(); + } + + @Test + public void onScrollStateChanged_settlingStartedWhenWasIdle_listenerNotifiedAboutScrollStart() { + layoutManager.currentScrollState = RecyclerView.SCROLL_STATE_IDLE; + + layoutManager.onScrollStateChanged(RecyclerView.SCROLL_STATE_SETTLING); + + verify(mockScrollStateListener).onScrollStart(); + } + + @Test + public void onScrollStateChanged_newState_layoutManagerUpdatesItsState() { + final int newState = RecyclerView.SCROLL_STATE_DRAGGING; + layoutManager.currentScrollState = RecyclerView.SCROLL_STATE_IDLE; + assertThat(layoutManager.currentScrollState, is(not(newState))); + + layoutManager.onScrollStateChanged(newState); + + assertThat(layoutManager.currentScrollState, is(newState)); + } + + @Test + public void onScrollStateChanged_scrolledEnoughToChangeCurrent_listenerNotifiedAboutScrollEnd() { + layoutManager.currentScrollState = RecyclerView.SCROLL_STATE_DRAGGING; + layoutManager.scrolled = layoutManager.scrollToChangeCurrent; + + layoutManager.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE); + + verify(mockScrollStateListener).onScrollEnd(); + } + + @Test + public void onScrollStateChanged_scrolledNotEnoughToChangeCurrent_listenerNotNotifiedAboutScrollEnd() { + layoutManager.currentScrollState = RecyclerView.SCROLL_STATE_DRAGGING; + layoutManager.scrollToChangeCurrent = stubOrientationHelper.getDistanceToChangeCurrent(CHILD_WIDTH, CHILD_HEIGHT); + layoutManager.scrolled = layoutManager.scrollToChangeCurrent / 2; + + layoutManager.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE); + + verify(mockScrollStateListener, never()).onScrollEnd(); + } + + @Test + public void onScrollStateChanged_draggedLessThanScrollToSnapToAnotherItem_settlesToCurrentPosition() { + layoutManager.currentScrollState = RecyclerView.SCROLL_STATE_DRAGGING; + layoutManager.currentPosition = ADAPTER_ITEM_COUNT / 2; + layoutManager.scrollToChangeCurrent = stubOrientationHelper.getDistanceToChangeCurrent(CHILD_WIDTH, CHILD_HEIGHT); + layoutManager.pendingScroll = 0; + layoutManager.scrolled = (int) (layoutManager.scrollToChangeCurrent * (SCROLL_TO_SNAP_TO_ANOTHER_ITEM - 0.01f)); + + layoutManager.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE); + + assertThat(layoutManager.pendingScroll, is(-layoutManager.scrolled)); + verify(stubRecyclerViewProxy).startSmoothScroll(any(RecyclerView.SmoothScroller.class)); + } + + @Test + public void onScrollStateChanged_draggedMoreThanOrScrollToSnapToAnotherItem_settlesToClosestItem() { + layoutManager.currentScrollState = RecyclerView.SCROLL_STATE_DRAGGING; + layoutManager.currentPosition = ADAPTER_ITEM_COUNT / 2; + layoutManager.scrollToChangeCurrent = stubOrientationHelper.getDistanceToChangeCurrent(CHILD_WIDTH, CHILD_HEIGHT); + layoutManager.pendingScroll = 0; + layoutManager.scrolled = (int) (layoutManager.scrollToChangeCurrent * SCROLL_TO_SNAP_TO_ANOTHER_ITEM); + int scrollLeftToAnotherItem = layoutManager.scrollToChangeCurrent - layoutManager.scrolled; + + layoutManager.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE); + + assertThat(layoutManager.pendingScroll, is(scrollLeftToAnotherItem)); + verify(stubRecyclerViewProxy).startSmoothScroll(any(RecyclerView.SmoothScroller.class)); + } + + @Test + public void onScrollStateChanged_whenSettlingDragIsStarted_settlingStops() { + layoutManager.currentScrollState = RecyclerView.SCROLL_STATE_SETTLING; + layoutManager.pendingScroll = 1000; + + layoutManager.onScrollStateChanged(RecyclerView.SCROLL_STATE_DRAGGING); + + assertThat(layoutManager.pendingScroll, is(0)); + } + + @Test + public void onScrollStateChanged_whenSettlingDragIsStarted_closestPositionBecomesCurrent() { + final int initialPosition = 5; + layoutManager.currentScrollState = RecyclerView.SCROLL_STATE_SETTLING; + layoutManager.scrollToChangeCurrent = stubOrientationHelper.getDistanceToChangeCurrent(CHILD_WIDTH, CHILD_HEIGHT); + final int scrolled = (int) (layoutManager.scrollToChangeCurrent * SCROLL_TO_SNAP_TO_ANOTHER_ITEM); + layoutManager.scrolled = scrolled; + layoutManager.currentPosition = initialPosition; + + layoutManager.onScrollStateChanged(RecyclerView.SCROLL_STATE_DRAGGING); + + assertThat(layoutManager.currentPosition, is(initialPosition + 1)); + assertThat(layoutManager.scrolled, is(scrolled - layoutManager.scrollToChangeCurrent)); + } + + @Test + public void onFling_velocitiesWithDifferentSignsOnDifferentAxis_correctFlingDirection() { + final int velocityX = 100, velocityY = -100; + final int velocityToUse = stubOrientationHelper.getFlingVelocity(velocityX, velocityY); + Direction direction = Direction.fromDelta(velocityToUse); + layoutManager.currentPosition = ADAPTER_ITEM_COUNT / 2; + layoutManager.scrollToChangeCurrent = stubOrientationHelper.getDistanceToChangeCurrent(CHILD_WIDTH, CHILD_HEIGHT); + layoutManager.pendingScroll = 0; + + layoutManager.onFling(velocityX, velocityY); + + assertThat(layoutManager.pendingScroll, is(direction.applyTo(layoutManager.scrollToChangeCurrent))); + } + + @Test + public void onFling_toTheOppositeToScrollDirection_returnsToPosition() { + layoutManager.scrollToChangeCurrent = stubOrientationHelper.getDistanceToChangeCurrent(CHILD_WIDTH, CHILD_HEIGHT); + int scrolled = layoutManager.scrollToChangeCurrent / 2; + layoutManager.pendingScroll = 0; + layoutManager.scrolled = scrolled; + + layoutManager.onFling(-scrolled, -scrolled); + + assertThat(layoutManager.pendingScroll, is(-scrolled)); + } + + @Test + public void onFling_toTheSameDirectionAsScrolled_changesPosition() { + layoutManager.scrollToChangeCurrent = stubOrientationHelper.getDistanceToChangeCurrent(CHILD_WIDTH, CHILD_HEIGHT); + int scrolled = layoutManager.scrollToChangeCurrent / 3; + int leftToScroll = layoutManager.scrollToChangeCurrent - scrolled; + layoutManager.pendingScroll = 0; + layoutManager.scrolled = scrolled; + + layoutManager.onFling(scrolled, scrolled); + + assertThat(layoutManager.pendingScroll, is(leftToScroll)); + } + + @Test + public void onFling_toItemBeforeTheFirst_isImpossible() { + layoutManager.currentPosition = 0; + + layoutManager.onFling(-100, -100); + + assertThat(layoutManager.pendingScroll, is(0)); + } + + @Test + public void onFling_toItemAfterTheLast_isImpossible() { + layoutManager.currentPosition = ADAPTER_ITEM_COUNT - 1; + + layoutManager.onFling(100, 100); + + assertThat(layoutManager.pendingScroll, is(0)); + } + + private void prepareStubsForScrollBy(int allowedScroll, int childCount, boolean hasNewBecomeVisible) { + doReturn(allowedScroll).when(layoutManager).calculateAllowedScrollIn(any(Direction.class)); + stubRecyclerViewProxy.addChildren(childCount, 0); + doReturn(hasNewBecomeVisible).when(stubOrientationHelper).hasNewBecomeVisible(any(DiscreteScrollLayoutManager.class)); + } + +} diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/HorizontalDiscreteScrollLayoutManagerTest.java b/library/src/test/java/com/yarolegovich/discretescrollview/HorizontalDiscreteScrollLayoutManagerTest.java new file mode 100644 index 0000000..b85403f --- /dev/null +++ b/library/src/test/java/com/yarolegovich/discretescrollview/HorizontalDiscreteScrollLayoutManagerTest.java @@ -0,0 +1,19 @@ +package com.yarolegovich.discretescrollview; + +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Created by yarolegovich on 10/28/17. + */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class HorizontalDiscreteScrollLayoutManagerTest extends DiscreteScrollLayoutManagerTest { + + @Override + protected Orientation getOrientationToTest() { + return Orientation.HORIZONTAL; + } + +} diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/VerticalDiscreteScrollLayoutManagerTest.java b/library/src/test/java/com/yarolegovich/discretescrollview/VerticalDiscreteScrollLayoutManagerTest.java new file mode 100644 index 0000000..f0fa481 --- /dev/null +++ b/library/src/test/java/com/yarolegovich/discretescrollview/VerticalDiscreteScrollLayoutManagerTest.java @@ -0,0 +1,19 @@ +package com.yarolegovich.discretescrollview; + +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Created by yarolegovich on 10/28/17. + */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class VerticalDiscreteScrollLayoutManagerTest extends DiscreteScrollLayoutManagerTest { + + @Override + protected Orientation getOrientationToTest() { + return Orientation.VERTICAL; + } + +} From 1c2ff85071acdf0d92fb9e6a5d6b9ede61bdcbc1 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 3 Feb 2018 16:01:46 +0200 Subject: [PATCH 26/61] Updated dependencies and build tools, released new version Resolved naming ambiguity by changing Orientation to DSVOrientation (re #67) --- README.md | 4 ++-- build.gradle | 18 +++++++++------- gradle/wrapper/gradle-wrapper.properties | 4 ++-- library/build.gradle | 6 +++--- .../{Orientation.java => DSVOrientation.java} | 2 +- .../DiscreteScrollLayoutManager.java | 6 +++--- .../DiscreteScrollView.java | 6 +++--- sample/build.gradle | 21 +++++++++---------- .../sample/DiscreteScrollViewOptions.java | 2 +- .../sample/shop/ShopActivity.java | 5 ++--- 10 files changed, 37 insertions(+), 37 deletions(-) rename library/src/main/java/com/yarolegovich/discretescrollview/{Orientation.java => DSVOrientation.java} (99%) diff --git a/README.md b/README.md index e644fa8..2fbdf50 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.3.2' +compile 'com.yarolegovich:discrete-scrollview:1.4.0' ``` ## Sample @@ -45,7 +45,7 @@ scrollView.setAdapter(new YourAdapterImplementation()); ### API #### Layout ```java -scrollView.setOrientation(Orientation o); //Sets an orientation of the view +scrollView.setOrientation(DSVOrientation o); //Sets an orientation of the view scrollView.setOffscreenItems(count); //Reserve extra space equal to (childSize * count) on each side of the view ``` #### Related to the current item: diff --git a/build.gradle b/build.gradle index ada7dd2..b101594 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,11 @@ buildscript { repositories { jcenter() + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.3' - classpath 'com.novoda:bintray-release:0.4.0' + classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.novoda:bintray-release:0.8.0' } } @@ -12,6 +13,7 @@ allprojects { repositories { jcenter() maven { url "https://maven.google.com" } + google() } } @@ -23,13 +25,13 @@ ext { userOrg = 'yarolegovich' groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' - description = 'Scrollable list of items, where current item is centered and can be changed using swipes.' - publishVersion = '1.3.2' + description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' + publishVersion = '1.4.0' licences = ['Apache-2.0'] - compileSdkVersion = 26 - buildToolsVersion = '26.0.1' - targetSdkVersion = 25 + compileSdkVersion = 27 + buildToolsVersion = '26.0.2' + targetSdkVersion = 27 - supportLibVersion = '26.0.0' + supportLibVersion = '27.0.2' } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 094594c..b99b98f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 12 09:24:26 BST 2017 +#Sat Feb 03 15:44:30 EET 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/library/build.gradle b/library/build.gradle index 45ad896..2643003 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -2,12 +2,12 @@ apply plugin: 'com.android.library' apply plugin: 'com.novoda.bintray-release' android { - compileSdkVersion rootProject.ext.compileSdkVersion - buildToolsVersion rootProject.ext.buildToolsVersion + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion defaultConfig { minSdkVersion 14 - targetSdkVersion rootProject.ext.targetSdkVersion + targetSdkVersion rootProject.targetSdkVersion versionCode 1 versionName "1.0" } diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java b/library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java similarity index 99% rename from library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java rename to library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java index 9a5b3e8..55d0f1d 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java @@ -7,7 +7,7 @@ /** * Created by yarolegovich on 16.03.2017. */ -public enum Orientation { +public enum DSVOrientation { HORIZONTAL { @Override diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 5a1ed05..e631ff4 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -40,7 +40,7 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { private int scrollToChangeCurrent; private int currentScrollState; - private Orientation.Helper orientationHelper; + private DSVOrientation.Helper orientationHelper; private int scrolled; private int pendingScroll; @@ -67,7 +67,7 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { public DiscreteScrollLayoutManager( @NonNull Context c, @NonNull ScrollStateListener scrollStateListener, - @NonNull Orientation orientation) { + @NonNull DSVOrientation orientation) { this.context = c; this.timeForItemSettle = DEFAULT_TIME_FOR_ITEM_SETTLE; this.pendingPosition = NO_POSITION; @@ -533,7 +533,7 @@ public void setOffscreenItems(int offscreenItems) { requestLayout(); } - public void setOrientation(Orientation orientation) { + public void setOrientation(DSVOrientation orientation) { orientationHelper = orientation.createHelper(); removeAllViews(); requestLayout(); diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index 540e44d..c5baee8 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -23,7 +23,7 @@ public class DiscreteScrollView extends RecyclerView { public static final int NO_POSITION = DiscreteScrollLayoutManager.NO_POSITION; - private static final int DEFAULT_ORIENTATION = Orientation.HORIZONTAL.ordinal(); + private static final int DEFAULT_ORIENTATION = DSVOrientation.HORIZONTAL.ordinal(); private DiscreteScrollLayoutManager layoutManager; @@ -58,7 +58,7 @@ private void init(AttributeSet attrs) { layoutManager = new DiscreteScrollLayoutManager( getContext(), new ScrollStateListener(), - Orientation.values()[orientation]); + DSVOrientation.values()[orientation]); setLayoutManager(layoutManager); } @@ -112,7 +112,7 @@ public void setSlideOnFlingThreshold(int threshold){ layoutManager.setSlideOnFlingThreshold(threshold); } - public void setOrientation(Orientation orientation) { + public void setOrientation(DSVOrientation orientation) { layoutManager.setOrientation(orientation); } diff --git a/sample/build.gradle b/sample/build.gradle index 9704d96..25e1b00 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,12 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion rootProject.ext.compileSdkVersion - buildToolsVersion rootProject.ext.buildToolsVersion + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + defaultConfig { applicationId "com.yarolegovich.discretescrollview.sample" minSdkVersion 19 - targetSdkVersion rootProject.ext.targetSdkVersion + targetSdkVersion rootProject.targetSdkVersion versionCode 4 versionName "1.0" } @@ -20,15 +21,13 @@ android { } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - - compile "com.android.support:appcompat-v7:$supportLibVersion" - compile "com.android.support:cardview-v7:$supportLibVersion" - compile "com.android.support:design:$supportLibVersion" + implementation "com.android.support:appcompat-v7:$supportLibVersion" + implementation "com.android.support:cardview-v7:$supportLibVersion" + implementation "com.android.support:design:$supportLibVersion" - compile 'com.github.bumptech.glide:glide:3.7.0' + implementation 'com.github.bumptech.glide:glide:3.7.0' - compile 'com.yarolegovich:mp:1.0.5' + implementation 'com.yarolegovich:mp:1.0.9' - compile project(':library') + implementation project(':library') } diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java index 20c56a2..c693801 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java @@ -3,8 +3,8 @@ import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; +import android.preference.PreferenceManager; import android.support.design.widget.BottomSheetDialog; -import android.support.v7.preference.PreferenceManager; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.RecyclerView; import android.view.Menu; diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java index 54c0770..af450a9 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java @@ -1,7 +1,6 @@ package com.yarolegovich.discretescrollview.sample.shop; import android.os.Bundle; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.content.ContextCompat; @@ -13,7 +12,7 @@ import com.yarolegovich.discretescrollview.DiscreteScrollView; import com.yarolegovich.discretescrollview.InfiniteScrollAdapter; -import com.yarolegovich.discretescrollview.Orientation; +import com.yarolegovich.discretescrollview.DSVOrientation; import com.yarolegovich.discretescrollview.sample.DiscreteScrollViewOptions; import com.yarolegovich.discretescrollview.sample.R; import com.yarolegovich.discretescrollview.transform.ScaleTransformer; @@ -48,7 +47,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { shop = Shop.get(); data = shop.getData(); itemPicker = (DiscreteScrollView) findViewById(R.id.item_picker); - itemPicker.setOrientation(Orientation.HORIZONTAL); + itemPicker.setOrientation(DSVOrientation.HORIZONTAL); itemPicker.addOnItemChangedListener(this); infiniteAdapter = InfiniteScrollAdapter.wrap(new ShopAdapter(data)); itemPicker.setAdapter(infiniteAdapter); From 5ed5c2e8c9d04a3dc80ac546adfa5407f6e08be4 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 3 Feb 2018 18:04:33 +0200 Subject: [PATCH 27/61] Implemented LayoutManager scroll APIs This provides compatibility with scrollbars or any components which rely on RecyclerView.canScroll(Vertically|Horizontally) methods. re #77 --- README.md | 2 +- build.gradle | 2 +- .../DiscreteScrollLayoutManager.java | 52 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2fbdf50..78b4828 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.4.0' +compile 'com.yarolegovich:discrete-scrollview:1.4.1' ``` ## Sample diff --git a/build.gradle b/build.gradle index b101594..5c949dc 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' - publishVersion = '1.4.0' + publishVersion = '1.4.1' licences = ['Apache-2.0'] compileSdkVersion = 27 diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index e631ff4..d3250d9 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -477,6 +477,58 @@ private void startSmoothPendingScroll(int position){ startSmoothPendingScroll(); } + @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; From 94b6dce5489ccb600a1724da745b3ab5fa9ac477 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 13:17:56 +0200 Subject: [PATCH 28/61] Updated build file and resolved conflicts with master Added dependencies for instrumentation tests. Adapted unit tests to a new Mockito version. --- library/build.gradle | 18 ++++++++++------ .../DiscreteScrollLayoutManagerTest.java | 21 +++++++++++++------ ...zontalDiscreteScrollLayoutManagerTest.java | 4 ++-- ...rticalDiscreteScrollLayoutManagerTest.java | 4 ++-- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index 8c15841..4631fca 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -10,17 +10,23 @@ android { targetSdkVersion rootProject.targetSdkVersion versionCode 1 versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } } dependencies { - compile "com.android.support:appcompat-v7:$supportLibVersion" - compile "com.android.support:recyclerview-v7:$supportLibVersion" + implementation "com.android.support:appcompat-v7:$supportLibVersion" + implementation "com.android.support:recyclerview-v7:$supportLibVersion" + + testImplementation 'org.robolectric:robolectric:3.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.7.22' + testImplementation 'org.hamcrest:hamcrest-library:1.3' - testCompile 'org.robolectric:robolectric:3.0' - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' - testCompile 'org.hamcrest:hamcrest-library:1.3' + androidTestImplementation "com.android.support:appcompat-v7:$supportLibVersion" + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + androidTestImplementation 'org.hamcrest:hamcrest-library:1.3' } publish { diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java b/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java index 7953072..9399f94 100644 --- a/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java +++ b/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java @@ -14,11 +14,20 @@ import static com.yarolegovich.discretescrollview.DiscreteScrollLayoutManager.NO_POSITION; import static com.yarolegovich.discretescrollview.DiscreteScrollLayoutManager.SCROLL_TO_SNAP_TO_ANOTHER_ITEM; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.junit.Assert.*; -import static org.mockito.Mockito.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; /** * Created by yarolegovich on 10/25/17. @@ -34,7 +43,7 @@ public abstract class DiscreteScrollLayoutManagerTest { private DiscreteScrollLayoutManager layoutManager; private DiscreteScrollLayoutManager.ScrollStateListener mockScrollStateListener; private StubRecyclerViewProxy stubRecyclerViewProxy; - private Orientation.Helper stubOrientationHelper; + private DSVOrientation.Helper stubOrientationHelper; private RecyclerView.State stubState; @Before @@ -58,7 +67,7 @@ public void setUp() { layoutManager.setOrientationHelper(stubOrientationHelper); } - protected abstract Orientation getOrientationToTest(); + protected abstract DSVOrientation getOrientationToTest(); @Test public void onLayoutChildren_noItems_removesViewsAndResetsState() { @@ -70,7 +79,7 @@ public void onLayoutChildren_noItems_removesViewsAndResetsState() { layoutManager.onLayoutChildren(null, stubState); - verify(stubRecyclerViewProxy).removeAndRecycleAllViews(any(RecyclerView.Recycler.class)); + verify(stubRecyclerViewProxy).removeAndRecycleAllViews(nullable(RecyclerView.Recycler.class)); assertThat(layoutManager.pendingScroll, is(0)); assertThat(layoutManager.scrolled, is(0)); assertThat(layoutManager.pendingPosition, is(NO_POSITION)); @@ -192,7 +201,7 @@ public void recycleDetachedViewsAndClearCache_cacheIsClearedAndViewsAreRecycled( assertThat(layoutManager.detachedCache.size(), is(0)); verify(stubRecyclerViewProxy, times(views.size())) - .recycleView(argThat(isIn(views)), any(RecyclerView.Recycler.class)); + .recycleView(argThat(isIn(views)), nullable(RecyclerView.Recycler.class)); } @Test @@ -309,7 +318,7 @@ public void scrollBy_newViewBecomesVisible_fillIsCalled() { layoutManager.scrollBy(200, null); - verify(layoutManager).fill(any(RecyclerView.Recycler.class)); + verify(layoutManager).fill(nullable(RecyclerView.Recycler.class)); } @Test diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/HorizontalDiscreteScrollLayoutManagerTest.java b/library/src/test/java/com/yarolegovich/discretescrollview/HorizontalDiscreteScrollLayoutManagerTest.java index b85403f..971dbcd 100644 --- a/library/src/test/java/com/yarolegovich/discretescrollview/HorizontalDiscreteScrollLayoutManagerTest.java +++ b/library/src/test/java/com/yarolegovich/discretescrollview/HorizontalDiscreteScrollLayoutManagerTest.java @@ -12,8 +12,8 @@ public class HorizontalDiscreteScrollLayoutManagerTest extends DiscreteScrollLayoutManagerTest { @Override - protected Orientation getOrientationToTest() { - return Orientation.HORIZONTAL; + protected DSVOrientation getOrientationToTest() { + return DSVOrientation.HORIZONTAL; } } diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/VerticalDiscreteScrollLayoutManagerTest.java b/library/src/test/java/com/yarolegovich/discretescrollview/VerticalDiscreteScrollLayoutManagerTest.java index f0fa481..070382b 100644 --- a/library/src/test/java/com/yarolegovich/discretescrollview/VerticalDiscreteScrollLayoutManagerTest.java +++ b/library/src/test/java/com/yarolegovich/discretescrollview/VerticalDiscreteScrollLayoutManagerTest.java @@ -12,8 +12,8 @@ public class VerticalDiscreteScrollLayoutManagerTest extends DiscreteScrollLayoutManagerTest { @Override - protected Orientation getOrientationToTest() { - return Orientation.VERTICAL; + protected DSVOrientation getOrientationToTest() { + return DSVOrientation.VERTICAL; } } From 4967100d81b468f50febc1a901713847f8d5cd0f Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 13:51:34 +0200 Subject: [PATCH 29/61] Created a test context for DiscreteScrollView --- library/src/androidTest/AndroidManifest.xml | 15 +++ .../context/TestActivity.java | 111 ++++++++++++++++++ .../context/TestAdapter.java | 74 ++++++++++++ .../discretescrollview/context/TestData.java | 32 +++++ 4 files changed, 232 insertions(+) create mode 100644 library/src/androidTest/AndroidManifest.xml create mode 100644 library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java create mode 100644 library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java create mode 100644 library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java diff --git a/library/src/androidTest/AndroidManifest.xml b/library/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..403a802 --- /dev/null +++ b/library/src/androidTest/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java new file mode 100644 index 0000000..cb1eacb --- /dev/null +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java @@ -0,0 +1,111 @@ +package com.yarolegovich.discretescrollview.context; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.test.espresso.IdlingResource; +import android.support.test.espresso.idling.CountingIdlingResource; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.yarolegovich.discretescrollview.DiscreteScrollView; +import com.yarolegovich.discretescrollview.R; +import com.yarolegovich.discretescrollview.transform.ScaleTransformer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +/** + * Created by yarolegovich on 2/4/18. + */ + +public class TestActivity extends AppCompatActivity implements DiscreteScrollView.ScrollStateChangeListener { + + private DiscreteScrollView scrollView; + private TestAdapter adapter; + + private CountingIdlingResource expectedScrollEndCalls = new CountingIdlingResource( + "scrollEndCalls" + hashCode(), + true); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + setTheme(R.style.Theme_AppCompat_Light_NoActionBar); + + super.onCreate(savedInstanceState); + + ViewGroup root = createRootView(); + scrollView = createScrollViewIn(root); + + setContentView(root); + + adapter = new TestAdapter(generateTestData(10)); + scrollView.setAdapter(adapter); + scrollView.addScrollStateChangeListener(this); + } + + public DiscreteScrollView getScrollView() { + return scrollView; + } + + public TestAdapter getAdapter() { + return adapter; + } + + private DiscreteScrollView createScrollViewIn(ViewGroup root) { + DiscreteScrollView scrollView = new DiscreteScrollView(this); + FrameLayout.LayoutParams scrollViewLp = new FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT); + scrollViewLp.gravity = Gravity.CENTER; + scrollView.setLayoutParams(scrollViewLp); + root.addView(scrollView); + return scrollView; + } + + private ViewGroup createRootView() { + FrameLayout root = new FrameLayout(this); + root.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + return root; + } + + public void incrementExpectedScrollEndCalls() { + expectedScrollEndCalls.increment(); + } + + @Override + public void onScrollStart(@NonNull RecyclerView.ViewHolder currentItemHolder, int adapterPosition) { + } + + @Override + public void onScrollEnd(@NonNull RecyclerView.ViewHolder currentItemHolder, int adapterPosition) { + if (!expectedScrollEndCalls.isIdleNow()) { + expectedScrollEndCalls.decrement(); + } + } + + @Override + public void onScroll(float scrollPosition, int currentPosition, int newPosition, @Nullable RecyclerView.ViewHolder currentHolder, @Nullable RecyclerView.ViewHolder newCurrent) { + + } + + public @NonNull List getIdlingResources() { + return Collections.singletonList(expectedScrollEndCalls); + } + + private List generateTestData(int size) { + List result = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + result.add(new TestData()); + } + return result; + } +} diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java new file mode 100644 index 0000000..8db2f28 --- /dev/null +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java @@ -0,0 +1,74 @@ +package com.yarolegovich.discretescrollview.context; + +import android.content.Context; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.RecyclerView; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.util.List; + +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +/** + * Created by yarolegovich on 2/4/18. + */ + +public class TestAdapter extends RecyclerView.Adapter { + + private List data; + private RecyclerView recyclerView; + + public TestAdapter(List data) { + this.data = data; + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + this.recyclerView = recyclerView; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + float dp = parent.getResources().getDisplayMetrics().density; + ImageView iv = new ImageView(parent.getContext()); + iv.setLayoutParams(new ViewGroup.LayoutParams((int) (180 * dp), (int) (256 * dp))); + iv.setScaleType(ImageView.ScaleType.CENTER_CROP); + return new ViewHolder(iv); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + TestData item = data.get(position); + holder.image.setImageDrawable(item.image); + } + + @Override + public int getItemCount() { + return data.size(); + } + + public List getData() { + return data; + } + + class ViewHolder extends RecyclerView.ViewHolder { + + public final ImageView image; + + public ViewHolder(View itemView) { + super(itemView); + image = (ImageView) itemView; + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + recyclerView.smoothScrollToPosition(getAdapterPosition()); + } + }); + } + } +} diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java new file mode 100644 index 0000000..b04686e --- /dev/null +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java @@ -0,0 +1,32 @@ +package com.yarolegovich.discretescrollview.context; + +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; + +import java.util.Random; + +/** + * Created by yarolegovich on 2/4/18. + */ + +public class TestData { + + private static int NEXT_ID = 1; + private static final Random random = new Random(); + + public final int id; + public final Drawable image; + public TestData() { + id = NEXT_ID++; + image = new ColorDrawable(generateRandomColor()); + } + + private static @ColorInt int generateRandomColor() { + return Color.argb(255, + random.nextInt(256), + random.nextInt(256), + random.nextInt(256)); + } +} From e118d0ea462d8f8ee412aeb43a62fb75c707f8de Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 13:52:37 +0200 Subject: [PATCH 30/61] Utility classes for integration tests This code will be shared between different test classes. --- .../DiscreteScrollViewTest.java | 85 +++++++++++++++++++ .../custom/CustomAssertions.java | 73 ++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java create mode 100644 library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java new file mode 100644 index 0000000..5a34111 --- /dev/null +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java @@ -0,0 +1,85 @@ +package com.yarolegovich.discretescrollview; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.CallSuper; +import android.support.test.espresso.Espresso; +import android.support.test.espresso.IdlingRegistry; +import android.support.test.espresso.IdlingResource; +import android.support.test.espresso.ViewInteraction; +import android.support.test.espresso.idling.CountingIdlingResource; +import android.support.test.rule.ActivityTestRule; +import android.view.View; + +import com.yarolegovich.discretescrollview.context.TestActivity; +import com.yarolegovich.discretescrollview.context.TestAdapter; + +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; + +import java.util.List; + +import static com.yarolegovich.discretescrollview.custom.CustomAssertions.currentPositionIs; +import static org.junit.Assert.assertThat; + +/** + * Created by yarolegovich on 2/3/18. + */ + +public abstract class DiscreteScrollViewTest { + + private IdlingResource[] idlingResources; + + protected DiscreteScrollView scrollView; + protected TestAdapter adapter; + + @Rule + public ActivityTestRule testActivity = new ActivityTestRule<>(TestActivity.class); + + @Before + @CallSuper + public void setUp() { + TestActivity activity = testActivity.getActivity(); + scrollView = activity.getScrollView(); + adapter = testActivity.getActivity().getAdapter(); + + List resources = activity.getIdlingResources(); + idlingResources = resources.toArray(new IdlingResource[resources.size()]); + IdlingRegistry.getInstance().register(idlingResources); + } + + @After + @CallSuper + public void tearDown() { + IdlingRegistry.getInstance().unregister(idlingResources); + } + + protected ViewInteraction onScrollView() { + return Espresso.onView(Matchers.is(scrollView)); + } + + protected void waitUntilScrollEnd() { + testActivity.getActivity().incrementExpectedScrollEndCalls(); + } + + protected void ensurePositionIs(final int position) { + onUiThread(new Runnable() { + @Override + public void run() { + scrollView.scrollToPosition(position); + } + }); + onScrollView().check(currentPositionIs(position)); + } + + protected void onUiThread(Runnable runnable) { + try { + testActivity.runOnUiThread(runnable); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + +} diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java new file mode 100644 index 0000000..8e65e31 --- /dev/null +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java @@ -0,0 +1,73 @@ +package com.yarolegovich.discretescrollview.custom; + +import android.support.test.espresso.NoMatchingViewException; +import android.support.test.espresso.ViewAssertion; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import com.yarolegovich.discretescrollview.DiscreteScrollView; + +import static org.hamcrest.Matchers.*; +import static android.support.test.espresso.matcher.ViewMatchers.*; + + +/** + * Created by yarolegovich on 2/3/18. + */ + +public class CustomAssertions { + + public static ViewAssertion currentPositionIs(final int expectedPosition) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewFoundException) { + ensureViewFound(noViewFoundException); + assertThat(view, isAssignableFrom(DiscreteScrollView.class)); + DiscreteScrollView dsv = (DiscreteScrollView) view; + assertThat(dsv.getCurrentItem(), is(equalTo(expectedPosition))); + + View midChild = findCenteredChildIn(dsv); + assertThat(midChild, is(notNullValue())); + RecyclerView.ViewHolder holder = dsv.getChildViewHolder(midChild); + assertThat(holder.getAdapterPosition(), is(equalTo(expectedPosition))); + } + }; + } + + public static ViewAssertion doesNotHaveChildren() { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewFoundException) { + ensureViewFound(noViewFoundException); + assertThat(view, isAssignableFrom(ViewGroup.class)); + ViewGroup viewGroup = (ViewGroup) view; + assertThat(viewGroup.getChildCount(), is(equalTo(0))); + } + }; + } + + private static View findCenteredChildIn(DiscreteScrollView dsv) { + final int centerX = dsv.getWidth() / 2; + final int centerY = dsv.getHeight() / 2; + for (int i = 0; i < dsv.getChildCount(); i++) { + View child = dsv.getChildAt(i); + if (centerX == (child.getLeft() + child.getWidth() / 2) + && centerY == (child.getTop() + child.getHeight() / 2)) { + return child; + } + } + throw new AssertionError("can't find centered child"); + } + + private static boolean isMidpoint(int value, int rangeStart, int rangeEnd) { + return value == (rangeStart + rangeEnd) / 2; + } + + private static void ensureViewFound(NoMatchingViewException exception) { + if (exception != null) { + throw exception; + } + } +} From 712b5a4cdbd9ea0336d71c528411a9e0b01099e2 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 13:54:28 +0200 Subject: [PATCH 31/61] Scroll functionality integration tests A few tests for scrollToPosition and smoothScrollToPosition methods --- .../ScrollFunctionalityTest.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java new file mode 100644 index 0000000..1ff4561 --- /dev/null +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java @@ -0,0 +1,110 @@ +package com.yarolegovich.discretescrollview; + +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static android.support.test.espresso.matcher.ViewMatchers.assertThat; +import static com.yarolegovich.discretescrollview.custom.CustomAssertions.currentPositionIs; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; + +/** + * Created by yarolegovich on 2/5/18. + */ +@RunWith(AndroidJUnit4.class) +public class ScrollFunctionalityTest extends DiscreteScrollViewTest { + + @Test + public void scrollToPosition_afterCurrent_changesPosition() { + final int initialPosition = scrollView.getCurrentItem(); + assertThat(initialPosition, is(lessThan(adapter.getItemCount() - 1))); + onUiThread(new Runnable() { + @Override + public void run() { + scrollView.scrollToPosition(initialPosition + 1); + } + }); + onScrollView().check(currentPositionIs(initialPosition + 1)); + } + + @Test + public void scrollToPosition_beforeCurrent_changesPosition() { + assertThat(adapter.getItemCount(), is(greaterThan(1))); + final int initialPosition = adapter.getItemCount() / 2; + ensurePositionIs(initialPosition); + onUiThread(new Runnable() { + @Override + public void run() { + scrollView.scrollToPosition(initialPosition - 1); + } + }); + onScrollView().check(currentPositionIs(initialPosition - 1)); + } + + @Test + public void smoothScrollToPosition_afterCurrent_changesPosition() { + final int initialPosition = scrollView.getCurrentItem(); + assertThat(initialPosition, is(lessThan(adapter.getItemCount() - 1))); + waitUntilScrollEnd(); + onUiThread(new Runnable() { + @Override + public void run() { + scrollView.setItemTransitionTimeMillis(10); + scrollView.smoothScrollToPosition(initialPosition + 1); + } + }); + onScrollView().check(currentPositionIs(initialPosition + 1)); + } + + @Test + public void smoothScrollToPosition_beforeCurrent_changesPosition() { + assertThat(adapter.getItemCount(), is(greaterThan(1))); + final int initialPosition = adapter.getItemCount() / 2; + ensurePositionIs(initialPosition); + waitUntilScrollEnd(); + onUiThread(new Runnable() { + @Override + public void run() { + scrollView.setItemTransitionTimeMillis(10); + scrollView.smoothScrollToPosition(initialPosition - 1); + } + }); + onScrollView().check(currentPositionIs(initialPosition - 1)); + } + + @Test + public void smoothScrollToPosition_throughSeveralPositionsAfterCurrent_changesPosition() { + final int initialPosition = scrollView.getCurrentItem(); + final int targetPosition = adapter.getItemCount() - 1; + assertThat(targetPosition - initialPosition, is(greaterThan(1))); + waitUntilScrollEnd(); + onUiThread(new Runnable() { + @Override + public void run() { + scrollView.setItemTransitionTimeMillis(10); + scrollView.smoothScrollToPosition(targetPosition); + } + }); + onScrollView().check(currentPositionIs(targetPosition)); + } + + @Test + public void smoothScrollToPosition_throughSeveralPositionsBeforeCurrent_changesPosition() { + final int initialPosition = adapter.getItemCount() - 1; + final int targetPosition = 0; + ensurePositionIs(initialPosition); + assertThat(initialPosition, is(greaterThan(0))); + waitUntilScrollEnd(); + onUiThread(new Runnable() { + @Override + public void run() { + scrollView.setItemTransitionTimeMillis(10); + scrollView.smoothScrollToPosition(targetPosition); + } + }); + onScrollView().check(currentPositionIs(targetPosition)); + } +} From 2c8d2ecaa937b9ac35254e761b460632b44e338c Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 13:55:08 +0200 Subject: [PATCH 32/61] Data set modification integration tests Tests for cases when adapter data changes. --- .../DataSetModificationTest.java | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java new file mode 100644 index 0000000..8640f10 --- /dev/null +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java @@ -0,0 +1,284 @@ +package com.yarolegovich.discretescrollview; + +import android.support.test.runner.AndroidJUnit4; + +import com.yarolegovich.discretescrollview.context.TestData; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import static android.support.test.espresso.matcher.ViewMatchers.*; +import static com.yarolegovich.discretescrollview.custom.CustomAssertions.currentPositionIs; +import static com.yarolegovich.discretescrollview.custom.CustomAssertions.doesNotHaveChildren; +import static org.hamcrest.Matchers.*; + +/** + * Created by yarolegovich on 2/3/18. + */ +@RunWith(AndroidJUnit4.class) +public class DataSetModificationTest extends DiscreteScrollViewTest { + + @Test + public void notifyItemInserted_afterCurrentPosition_currentPositionIsNotAffected() { + final int initialPosition = scrollView.getCurrentItem(); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + data.add(initialPosition + 1, new TestData()); + adapter.notifyItemInserted(initialPosition + 1); + } + }); + onScrollView().check(currentPositionIs(initialPosition)); + } + + @Test + public void notifyItemInserted_beforeCurrentPosition_currentPositionIsShifterRightByOne() { + final int initialPosition = scrollView.getCurrentItem(); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + data.add(initialPosition, new TestData()); + adapter.notifyItemInserted(initialPosition); + } + }); + onScrollView().check(currentPositionIs(initialPosition + 1)); + } + + @Test + public void notifyItemRemoved_afterCurrentPosition_currentPositionIsNotAffected() { + final int initialPosition = scrollView.getCurrentItem(); + assertThat(adapter.getItemCount(), is(greaterThan(1))); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + data.remove(initialPosition + 1); + adapter.notifyItemRemoved(initialPosition + 1); + } + }); + onScrollView().check(currentPositionIs(initialPosition)); + } + + @Test + public void notifyItemRemoved_beforeCurrentPosition_currentPositionIsShifterLeftByOne() { + assertThat(adapter.getItemCount(), is(greaterThan(1))); + final int initialPosition = adapter.getItemCount() / 2; + ensurePositionIs(initialPosition); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + data.remove(initialPosition - 1); + adapter.notifyItemRemoved(initialPosition - 1); + } + }); + onScrollView().check(currentPositionIs(initialPosition - 1)); + } + + @Test + public void notifyItemInserted_multipleInsertsBeforeCurrent_currentIsShiftedCorrectly() { + final int numberOfInserts = 5; + final int initialPosition = scrollView.getCurrentItem(); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + for (int i = 0; i < numberOfInserts; i++) { + data.add(initialPosition, new TestData()); + adapter.notifyItemInserted(initialPosition); + } + } + }); + onScrollView().check(currentPositionIs(initialPosition + numberOfInserts)); + } + + @Test + public void notifyItemRemoved_calledUntilEmpty_scrollViewIsEmpty() { + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + while (data.size() > 0) { + data.remove(0); + adapter.notifyItemRemoved(0); + } + } + }); + onScrollView().check(doesNotHaveChildren()); + } + + @Test + public void notifyItemRangeInserted_afterCurrentPosition_positionIsNotAffected() { + final int numOfItemsToInsert = 5; + final int initialPosition = scrollView.getCurrentItem(); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + data.addAll(initialPosition + 1, createItems(numOfItemsToInsert)); + adapter.notifyItemRangeInserted(initialPosition + 1, numOfItemsToInsert); + } + }); + onScrollView().check(currentPositionIs(initialPosition)); + } + + @Test + public void notifyItemRangeInserted_beforeCurrentPosition_positionIsShiftedRightByRangeLength() { + final int numOfItemsToInsert = 5; + final int initialPosition = scrollView.getCurrentItem(); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + data.addAll(initialPosition, createItems(numOfItemsToInsert)); + adapter.notifyItemRangeInserted(initialPosition, numOfItemsToInsert); + } + }); + onScrollView().check(currentPositionIs(initialPosition + numOfItemsToInsert)); + } + + @Test + public void notifyItemRangeRemoved_afterCurrentPosition_positionIsNotAffected() { + final int initialPosition = scrollView.getCurrentItem(); + final int initialSize = adapter.getItemCount(); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + List toRemove = new ArrayList<>(); + for (int i = initialPosition + 1; i < adapter.getItemCount() - 1; i++) { + toRemove.add(data.get(i)); + } + assertThat(toRemove.size(), is(greaterThan(1))); + data.removeAll(toRemove); + assertThat(data.size(), is(equalTo(initialSize - toRemove.size()))); + adapter.notifyItemRangeRemoved(initialPosition + 1, toRemove.size()); + } + }); + onScrollView().check(currentPositionIs(initialPosition)); + } + + @Test + public void notifyItemRangeRemoved_beforeCurrentPosition_positionIsShiftedLeftByRangeLength() { + assertThat(adapter.getItemCount(), is(greaterThan(2))); + final int initialPosition = adapter.getItemCount() - 1; + final int numOfItemsToRemove = adapter.getItemCount() - 1; + ensurePositionIs(initialPosition); + onUiThread(new Runnable() { + @Override + public void run() { + final int initialSize = adapter.getItemCount(); + List data = adapter.getData(); + List toRemove = new ArrayList<>(); + for (int i = initialPosition - 1; i >= 0; i--) { + toRemove.add(data.get(i)); + } + assertThat(toRemove.size(), is(equalTo(numOfItemsToRemove))); + data.removeAll(toRemove); + assertThat(data.size(), is(equalTo(initialSize - toRemove.size()))); + adapter.notifyItemRangeRemoved(0, toRemove.size()); + } + }); + onScrollView().check(currentPositionIs(initialPosition - numOfItemsToRemove)); + } + + @Test + public void notifyDataSetChanged_currentItemRemainsInItemRange_currentIsNotAffected() { + final int initialPosition = 0; + ensurePositionIs(initialPosition); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + assertThat(data.size(), is(greaterThan(2))); + data.remove(data.size() - 1); + data.remove(data.size() - 1); + adapter.notifyDataSetChanged(); + } + }); + onScrollView().check(currentPositionIs(initialPosition)); + } + + @Test + public void notifyDataSetChanged_currentItemGoesOutsideItemRange_currentIsClampedToRange() { + final int initialPosition = adapter.getItemCount() - 1; + final int numOfItemsToRemove = 2; + assertThat(adapter.getItemCount(), is(greaterThan(numOfItemsToRemove))); + ensurePositionIs(initialPosition); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + for (int i = numOfItemsToRemove - 1; i >= 0; i--) { + data.remove(i); + } + adapter.notifyDataSetChanged(); + } + }); + onScrollView().check(currentPositionIs(adapter.getItemCount() - 1)); + } + + @Test + public void notifyDataSetChanged_allItemsRemoved_scrollViewIsEmpty() { + onUiThread(new Runnable() { + @Override + public void run() { + adapter.getData().clear(); + adapter.notifyDataSetChanged(); + } + }); + onScrollView().check(doesNotHaveChildren()); + } + + @Test + public void notifyDataSetChanged_scrollToPositionCalledAfterItemsAdded_positionIsCorrect() { + final int targetPosition = adapter.getItemCount(); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + final int itemsToAdd = data.size(); + for (int i = 0; i < itemsToAdd; i++) { + data.add(new TestData()); + } + adapter.notifyDataSetChanged(); + scrollView.scrollToPosition(targetPosition); + } + }); + onScrollView().check(currentPositionIs(targetPosition)); + } + + @Test + public void notifyDataSetChanged_scrollToPositionCalledAfterItemsRemoved_positionIsCorrect() { + final int initialPosition = adapter.getItemCount() - 1; + final int targetPosition = adapter.getItemCount() / 4; + assertThat(targetPosition, is(greaterThan(0))); + ensurePositionIs(initialPosition); + onUiThread(new Runnable() { + @Override + public void run() { + List data = adapter.getData(); + final int itemsToRemove = data.size() / 2; + for (int i = 0; i < itemsToRemove; i++) { + data.remove(0); + } + adapter.notifyDataSetChanged(); + scrollView.scrollToPosition(targetPosition); + } + }); + onScrollView().check(currentPositionIs(targetPosition)); + } + + private List createItems(int count) { + List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + result.add(new TestData()); + } + return result; + } +} From ff2c29429ee1b7f8740c142ac2e53a99ac96fb07 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 16:20:12 +0200 Subject: [PATCH 33/61] InfiniteScrollAdapter minor improvements The adapter behaves as normal if there is only one item. re #62 --- .../InfiniteScrollAdapter.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java index e54dab2..866c5ba 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java @@ -4,6 +4,8 @@ import android.support.v7.widget.RecyclerView; import android.view.ViewGroup; +import java.util.Locale; + /** * Created by yarolegovich on 28-Apr-17. */ @@ -66,7 +68,7 @@ public int getItemViewType(int position) { @Override public int getItemCount() { - return wrapped.getItemCount() == 0 ? 0 : Integer.MAX_VALUE; + return wrapped.getItemCount() <= 1 ? wrapped.getItemCount() : Integer.MAX_VALUE; } public int getRealItemCount() { @@ -82,6 +84,11 @@ public int getRealPosition(int position) { } public int getClosestPosition(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())); + } int adapterTarget = currentRangeStart + position; int adapterCurrent = layoutManager.getCurrentPosition(); if (adapterTarget == adapterCurrent) { @@ -117,8 +124,13 @@ private int mapPositionToReal(int position) { } private void resetRange(int newPosition) { - currentRangeStart = Integer.MAX_VALUE / 2; - layoutManager.scrollToPosition(currentRangeStart + newPosition); + if (getItemCount() == 1) { + currentRangeStart = 0; + layoutManager.scrollToPosition(0); + } else { + currentRangeStart = Integer.MAX_VALUE / 2; + layoutManager.scrollToPosition(currentRangeStart + newPosition); + } } //TODO: handle proper data set change notifications From 3fe82fe0a69d40d69374711bf89666df3b6206c2 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 17:56:11 +0200 Subject: [PATCH 34/61] API for gradual item transform (re #37) --- README.md | 4 ++++ .../DiscreteScrollLayoutManager.java | 16 +++++++++++++--- .../discretescrollview/DiscreteScrollView.java | 7 +++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 78b4828..03321d9 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,10 @@ public interface DiscreteScrollItemTransformer { void transformItem(View item, float position); } ``` +In the above example `view1Position == (currentlySelectedViewPosition - n)` and `view2Position == (currentlySelectedViewPosition + n)`, where `n` defaults to 1 and can be changed using the following API: +```java +scrollView.setClampTransformProgressAfter(n); +``` Because scale transformation is the most common, I included a helper class - ScaleTransformer, here is how to use it: ```java cityPicker.setItemTransformer(new ScaleTransformer.Builder() diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index f82cb41..9daa246 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -28,6 +28,7 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { 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; @@ -57,6 +58,7 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { private int timeForItemSettle; private int offscreenItems; + private int transformClampItemCount; private boolean dataSetChangeShiftedPosition; @@ -86,6 +88,7 @@ public DiscreteScrollLayoutManager( this.scrollStateListener = scrollStateListener; this.orientationHelper = orientation.createHelper(); this.recyclerViewProxy = new RecyclerViewProxy(this); + this.transformClampItemCount = DEFAULT_TRANSFORM_CLAMP_ITEM_COUNT; setAutoMeasureEnabled(true); } @@ -315,9 +318,11 @@ protected int scrollBy(int amount, RecyclerView.Recycler recycler) { protected void applyItemTransformToChildren() { if (itemTransformer != null) { + int clampAfterDistance = scrollToChangeCurrent * transformClampItemCount; for (int i = 0; i < recyclerViewProxy.getChildCount(); i++) { View child = recyclerViewProxy.getChildAt(i); - itemTransformer.transformItem(child, getCenterRelativePositionOf(child)); + float position = getCenterRelativePositionOf(child, clampAfterDistance); + itemTransformer.transformItem(child, position); } } } @@ -590,6 +595,11 @@ public void setOffscreenItems(int offscreenItems) { recyclerViewProxy.requestLayout(); } + public void setTransformClampItemCount(int transformClampItemCount) { + this.transformClampItemCount = transformClampItemCount; + applyItemTransformToChildren(); + } + public void setOrientation(DSVOrientation orientation) { orientationHelper = orientation.createHelper(); recyclerViewProxy.removeAllViews(); @@ -618,11 +628,11 @@ public void onInitializeAccessibilityEvent(AccessibilityEvent event) { } } - private float getCenterRelativePositionOf(View v) { + 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 / scrollToChangeCurrent), 1f); + return Math.min(Math.max(-1f, distanceFromCenter / maxDistance), 1f); } private int checkNewOnFlingPositionIsInBounds(int position) { diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index c5baee8..ab1de6d 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -120,6 +120,13 @@ 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 addScrollStateChangeListener(@NonNull ScrollStateChangeListener scrollStateChangeListener) { scrollStateChangeListeners.add(scrollStateChangeListener); } From 826176ba8484c6b2a387dc2814961629bcdc971f Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 18:02:58 +0200 Subject: [PATCH 35/61] Published 1.4.2 --- README.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 03321d9..4d7da0d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.4.1' +compile 'com.yarolegovich:discrete-scrollview:1.4.2' ``` ## Sample diff --git a/build.gradle b/build.gradle index 5c949dc..92e7bda 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' - publishVersion = '1.4.1' + publishVersion = '1.4.2' licences = ['Apache-2.0'] compileSdkVersion = 27 From 5c1e12f510ad9551b0abf0b7981603a999239ea7 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 20:31:12 +0200 Subject: [PATCH 36/61] Added an API to disable overscroll effect --- .../discretescrollview/DiscreteScrollView.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index ab1de6d..2e09c97 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -30,6 +30,8 @@ public class DiscreteScrollView extends RecyclerView { private List scrollStateChangeListeners; private List onItemChangedListeners; + private boolean isOverScrollEnabled; + public DiscreteScrollView(Context context) { super(context); init(null); @@ -56,6 +58,8 @@ private void init(AttributeSet attrs) { ta.recycle(); } + isOverScrollEnabled = getOverScrollMode() != OVER_SCROLL_NEVER; + layoutManager = new DiscreteScrollLayoutManager( getContext(), new ScrollStateListener(), DSVOrientation.values()[orientation]); @@ -127,6 +131,11 @@ public void setClampTransformProgressAfter(@IntRange(from = 1) int itemCount) { layoutManager.setTransformClampItemCount(itemCount); } + public void setOverScrollEnabled(boolean overScrollEnabled) { + isOverScrollEnabled = overScrollEnabled; + setOverScrollMode(OVER_SCROLL_NEVER); + } + public void addScrollStateChangeListener(@NonNull ScrollStateChangeListener scrollStateChangeListener) { scrollStateChangeListeners.add(scrollStateChangeListener); } @@ -192,7 +201,9 @@ private class ScrollStateListener implements DiscreteScrollLayoutManager.ScrollS @Override public void onIsBoundReachedFlagChange(boolean isBoundReached) { - setOverScrollMode(isBoundReached ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER); + if (isOverScrollEnabled) { + setOverScrollMode(isBoundReached ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER); + } } @Override From 67c1634f7992d7efea479448311ad6d6bb30a6d0 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 5 Feb 2018 20:39:54 +0200 Subject: [PATCH 37/61] Published 1.4.3 --- README.md | 5 +++-- build.gradle | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4d7da0d..45c90a7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.4.2' +compile 'com.yarolegovich:discrete-scrollview:1.4.3' ``` ## Sample @@ -43,10 +43,11 @@ scrollView.setAdapter(new YourAdapterImplementation()); ``` ### API -#### Layout +#### General ```java scrollView.setOrientation(DSVOrientation o); //Sets an orientation of the view scrollView.setOffscreenItems(count); //Reserve extra space equal to (childSize * count) on each side of the view +scrollView.setOverScrollEnabled(enabled); //Can also be set using android:overScrollMode xml attribute ``` #### Related to the current item: ```java diff --git a/build.gradle b/build.gradle index 92e7bda..fce2cd9 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' - publishVersion = '1.4.2' + publishVersion = '1.4.3' licences = ['Apache-2.0'] compileSdkVersion = 27 From fd74ec9a5ccaf17071f5720dd616c31a7062d5a1 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Tue, 6 Feb 2018 21:14:38 +0200 Subject: [PATCH 38/61] Correctly process child margins (re #80) --- .../DiscreteScrollLayoutManager.java | 4 ++-- .../discretescrollview/RecyclerViewProxy.java | 11 +++++++---- .../stub/StubRecyclerViewProxy.java | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 9daa246..e44bbb5 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -137,8 +137,8 @@ public void onLayoutCompleted(RecyclerView.State state) { protected void initChildDimensions(RecyclerView.Recycler recycler) { View viewToMeasure = recyclerViewProxy.getMeasuredChildForAdapterPosition(0, recycler); - int childViewWidth = recyclerViewProxy.getMeasuredWidth(viewToMeasure); - int childViewHeight = recyclerViewProxy.getMeasuredHeight(viewToMeasure); + int childViewWidth = recyclerViewProxy.getMeasuredWidthWithMargin(viewToMeasure); + int childViewHeight = recyclerViewProxy.getMeasuredHeightWithMargin(viewToMeasure); childHalfWidth = childViewWidth / 2; childHalfHeight = childViewHeight / 2; diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java b/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java index 213c39d..32d5309 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java @@ -4,6 +4,7 @@ import android.support.v7.widget.LinearSmoothScroller; import android.support.v7.widget.RecyclerView; import android.view.View; +import android.view.ViewGroup; /** * Created by yarolegovich on 10/25/17. @@ -67,12 +68,14 @@ public int getPosition(View view) { return layoutManager.getPosition(view); } - public int getMeasuredWidth(View child) { - return layoutManager.getDecoratedMeasuredWidth(child); + public int getMeasuredWidthWithMargin(View child) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams(); + return layoutManager.getDecoratedMeasuredWidth(child) + lp.leftMargin + lp.rightMargin; } - public int getMeasuredHeight(View child) { - return layoutManager.getDecoratedMeasuredHeight(child); + public int getMeasuredHeightWithMargin(View child) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams(); + return layoutManager.getDecoratedMeasuredHeight(child) + lp.topMargin + lp.bottomMargin; } public int getWidth() { diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java b/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java index d4b5753..bb5c3ca 100644 --- a/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java +++ b/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java @@ -67,12 +67,12 @@ public int getPosition(View view) { } @Override - public int getMeasuredWidth(View child) { + public int getMeasuredWidthWithMargin(View child) { return childWidth; } @Override - public int getMeasuredHeight(View child) { + public int getMeasuredHeightWithMargin(View child) { return childHeight; } From 7573f8691c9408706decca7f7d29934799db4b24 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Tue, 6 Feb 2018 21:17:48 +0200 Subject: [PATCH 39/61] Published 1.4.4 --- README.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 45c90a7..4e96425 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.4.3' +compile 'com.yarolegovich:discrete-scrollview:1.4.4' ``` ## Sample diff --git a/build.gradle b/build.gradle index fce2cd9..3fe9dec 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' - publishVersion = '1.4.3' + publishVersion = '1.4.4' licences = ['Apache-2.0'] compileSdkVersion = 27 From 107cb9cb26def8531e2db08912cc87647ee979b9 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 12 Feb 2018 18:07:00 +0200 Subject: [PATCH 40/61] Relayout views when RecyclerView changes dimensions re #85 --- .../DiscreteScrollLayoutManager.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index e44bbb5..207c2d3 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -65,6 +65,8 @@ class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager { private int flingThreshold; private boolean shouldSlideOnFling; + private int viewWidth, viewHeight; + @NonNull private final ScrollStateListener scrollStateListener; private DiscreteScrollItemTransformer itemTransformer; @@ -105,6 +107,10 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State currentPosition = 0; } + if (!state.isMeasuring()) { + checkRecyclerViewDimensionsChanged(); + } + //onLayoutChildren may be called multiple times and this check is required so that the flag //won't be cleared until onLayoutCompleted if (!isFirstOrEmptyLayout) { @@ -123,6 +129,14 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State applyItemTransformToChildren(); } + private void checkRecyclerViewDimensionsChanged() { + if (recyclerViewProxy.getWidth() != viewWidth || recyclerViewProxy.getHeight() != viewHeight) { + viewWidth = recyclerViewProxy.getWidth(); + viewHeight = recyclerViewProxy.getHeight(); + recyclerViewProxy.removeAllViews(); + } + } + @Override public void onLayoutCompleted(RecyclerView.State state) { if (isFirstOrEmptyLayout) { From 2c60e167d26488fc0ce330525eb9e7885e86f9a2 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 12 Feb 2018 18:13:54 +0200 Subject: [PATCH 41/61] Published 1.4.6 --- README.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e96425..f107dc1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.4.4' +compile 'com.yarolegovich:discrete-scrollview:1.4.6' ``` ## Sample diff --git a/build.gradle b/build.gradle index 3fe9dec..2711ce4 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' - publishVersion = '1.4.4' + publishVersion = '1.4.6' licences = ['Apache-2.0'] compileSdkVersion = 27 From 50b30833208eda0f219547d8ee4730a875217349 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Sun, 1 Apr 2018 12:26:17 +0300 Subject: [PATCH 42/61] Added "Reporting an issue" section to a README --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f107dc1..bf745a0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,12 @@ Add this into your dependencies block. compile 'com.yarolegovich:discrete-scrollview:1.4.6' ``` +## Reporting an issue + +If you are going to report an issue, I will greatly appreciate you including some code which I can run to see the issue. By doing so you maximize the chance that I will fix the problem. + +By the way, before reporting a problem, try replacing DiscreteScrollView with a RecyclerView. If the problem is still present, it's likely somewhere in your code. + ## Sample Get it on Google Play
@@ -193,4 +199,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -``` \ No newline at end of file +``` From 5dd10a14730921bb89a254cdc307c128c10b2a47 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 2 Apr 2018 10:30:23 +0300 Subject: [PATCH 43/61] Fixed incorrect state after rotation re #102 --- .../DiscreteScrollLayoutManager.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 207c2d3..8154605 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -91,7 +91,6 @@ public DiscreteScrollLayoutManager( this.orientationHelper = orientation.createHelper(); this.recyclerViewProxy = new RecyclerViewProxy(this); this.transformClampItemCount = DEFAULT_TRANSFORM_CLAMP_ITEM_COUNT; - setAutoMeasureEnabled(true); } @Override @@ -103,9 +102,7 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State return; } - if (currentPosition == NO_POSITION) { - currentPosition = 0; - } + ensureValidPosition(state); if (!state.isMeasuring()) { checkRecyclerViewDimensionsChanged(); @@ -129,6 +126,15 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State 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; + } + } + private void checkRecyclerViewDimensionsChanged() { if (recyclerViewProxy.getWidth() != viewWidth || recyclerViewProxy.getHeight() != viewHeight) { viewWidth = recyclerViewProxy.getWidth(); @@ -502,6 +508,11 @@ private void startSmoothPendingScroll(int position) { startSmoothPendingScroll(); } + @Override + public boolean isAutoMeasureEnabled() { + return true; + } + @Override public int computeVerticalScrollRange(RecyclerView.State state) { return computeScrollRange(state); From f032d3022e58731a7ab413571bfd32fbcb4a0143 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 2 Apr 2018 10:30:44 +0300 Subject: [PATCH 44/61] Published 1.4.7 --- README.md | 2 +- build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bf745a0..bff317f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.4.6' +compile 'com.yarolegovich:discrete-scrollview:1.4.7' ``` ## Reporting an issue diff --git a/build.gradle b/build.gradle index 2711ce4..c81f1ad 100644 --- a/build.gradle +++ b/build.gradle @@ -26,12 +26,12 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' - publishVersion = '1.4.6' + publishVersion = '1.4.7' licences = ['Apache-2.0'] compileSdkVersion = 27 buildToolsVersion = '26.0.2' targetSdkVersion = 27 - supportLibVersion = '27.0.2' + supportLibVersion = '27.1.0' } \ No newline at end of file From 4cd0215e1648eacbdd70bf95e6dc82845576fb61 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Wed, 18 Jul 2018 20:46:50 +0300 Subject: [PATCH 45/61] Prevent crash in smoothScrollToPosition Looking at the code, the call was happening either before the layout or when the adapter was empty: these are two cases when currentPosition is NO_POSITION (-1). re #112 --- .../DiscreteScrollLayoutManager.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 8154605..18c6b47 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -18,6 +18,8 @@ import com.yarolegovich.discretescrollview.transform.DiscreteScrollItemTransformer; +import java.util.Locale; + /** * Created by yarolegovich on 17.02.2017. */ @@ -362,7 +364,13 @@ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State if (currentPosition == position || pendingPosition != NO_POSITION) { return; } - startSmoothPendingScroll(position); + checkTargetPosition(state, position); + if (currentPosition == NO_POSITION) { + //Layout not happened yet + currentPosition = position; + } else { + startSmoothPendingScroll(position); + } } @Override @@ -711,6 +719,14 @@ private boolean isViewVisible(Point viewCenter, int endBound) { 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; } From 26fc9fd6dd92db8ec54ec50db90def8d94022840 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Wed, 18 Jul 2018 21:04:57 +0300 Subject: [PATCH 46/61] Fixed InfiniteAdapter::notifyItem(Range)?Changed calls The calls do not scroll the view to the first position. That was never necessary, actually. re #106 --- .../discretescrollview/InfiniteScrollAdapter.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java index 866c5ba..b70c9cd 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java @@ -148,23 +148,23 @@ public void onItemRangeRemoved(int positionStart, int itemCount) { } @Override - public void onItemRangeChanged(int positionStart, int itemCount) { + public void onItemRangeInserted(int positionStart, int itemCount) { onChanged(); } @Override - public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { onChanged(); } @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - onChanged(); + public void onItemRangeChanged(int positionStart, int itemCount) { + notifyItemRangeChanged(0, getItemCount()); } @Override - public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { - onChanged(); + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + notifyItemRangeChanged(0, getItemCount(), payload); } } } \ No newline at end of file From 3770e6b97dfeb612ee62b23dff0aada1dffeee9d Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Wed, 18 Jul 2018 21:12:12 +0300 Subject: [PATCH 47/61] Small LayoutManager refactoring Merged two methods with similar names into one. --- .../DiscreteScrollLayoutManager.java | 24 ++++++++----------- .../DiscreteScrollLayoutManagerTest.java | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 18c6b47..0690d42 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -106,9 +106,7 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State ensureValidPosition(state); - if (!state.isMeasuring()) { - checkRecyclerViewDimensionsChanged(); - } + updateRecyclerDimensions(state); //onLayoutChildren may be called multiple times and this check is required so that the flag //won't be cleared until onLayoutCompleted @@ -119,8 +117,6 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State } } - updateRecyclerDimensions(); - recyclerViewProxy.detachAndScrapAttachedViews(recycler); fill(recycler); @@ -137,14 +133,6 @@ private void ensureValidPosition(RecyclerView.State state) { } } - private void checkRecyclerViewDimensionsChanged() { - if (recyclerViewProxy.getWidth() != viewWidth || recyclerViewProxy.getHeight() != viewHeight) { - viewWidth = recyclerViewProxy.getWidth(); - viewHeight = recyclerViewProxy.getHeight(); - recyclerViewProxy.removeAllViews(); - } - } - @Override public void onLayoutCompleted(RecyclerView.State state) { if (isFirstOrEmptyLayout) { @@ -174,7 +162,15 @@ protected void initChildDimensions(RecyclerView.Recycler recycler) { recyclerViewProxy.detachAndScrapView(viewToMeasure, recycler); } - protected void updateRecyclerDimensions() { + 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() / 2, recyclerViewProxy.getHeight() / 2); diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java b/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java index 9399f94..f60ddcc 100644 --- a/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java +++ b/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java @@ -160,7 +160,7 @@ public void initChildDimensions_offscreenItemsSet_extraLayoutSpaceIsCalculated() public void updateRecyclerDimensions_recyclerCenterIsInitialized() { layoutManager.recyclerCenter.set(0, 0); - layoutManager.updateRecyclerDimensions(); + layoutManager.updateRecyclerDimensions(stubState); assertThat(layoutManager.recyclerCenter.x, is(RECYCLER_WIDTH / 2)); assertThat(layoutManager.recyclerCenter.y, is(RECYCLER_HEIGHT / 2)); From 51f9afe18fda2dd4ef3aa05c3bae3f0603b3cce6 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Wed, 18 Jul 2018 21:26:25 +0300 Subject: [PATCH 48/61] Updated dependencies --- build.gradle | 2 +- library/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c81f1ad..6044073 100644 --- a/build.gradle +++ b/build.gradle @@ -33,5 +33,5 @@ ext { buildToolsVersion = '26.0.2' targetSdkVersion = 27 - supportLibVersion = '27.1.0' + supportLibVersion = '27.1.1' } \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index 4631fca..99d325c 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -21,7 +21,7 @@ dependencies { testImplementation 'org.robolectric:robolectric:3.0' testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.7.22' + testImplementation 'org.mockito:mockito-core:2.13.0' testImplementation 'org.hamcrest:hamcrest-library:1.3' androidTestImplementation "com.android.support:appcompat-v7:$supportLibVersion" From 1d894c69dfe5ae95411026f6f5be6aae9e9c00e4 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Wed, 18 Jul 2018 21:30:51 +0300 Subject: [PATCH 49/61] Published 1.4.8 --- README.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bff317f..bea6f10 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.4.7' +compile 'com.yarolegovich:discrete-scrollview:1.4.8' ``` ## Reporting an issue diff --git a/build.gradle b/build.gradle index 6044073..212e4c6 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' - publishVersion = '1.4.7' + publishVersion = '1.4.8' licences = ['Apache-2.0'] compileSdkVersion = 27 From 4ce5fec24726991ab8cdd09b52ad5ec0bd21c133 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 30 Jul 2018 12:17:06 +0300 Subject: [PATCH 50/61] Changes to InfiniteScrollAdapter Do not use "sliding range" to simulate infinite scroll. I suppose that calls to resetRange() during the scroll were sometimes causing temporary issues with current position calculations when there were too few items in the adapter. re #125 --- .../DiscreteScrollLayoutManager.java | 9 +- .../InfiniteScrollAdapter.java | 106 ++++++++++-------- 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index 0690d42..be81625 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -573,7 +573,11 @@ private int computeScrollRange(RecyclerView.State state) { public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { pendingPosition = NO_POSITION; scrolled = pendingScroll = 0; - currentPosition = 0; + if (newAdapter instanceof InitialPositionProvider) { + currentPosition = ((InitialPositionProvider) newAdapter).getInitialPosition(); + } else { + currentPosition = 0; + } recyclerViewProxy.removeAllViews(); } @@ -776,4 +780,7 @@ public interface ScrollStateListener { void onDataSetChangeChangedPosition(); } + public interface InitialPositionProvider { + int getInitialPosition(); + } } diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java index b70c9cd..dacf70c 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java @@ -10,9 +10,10 @@ * Created by yarolegovich on 28-Apr-17. */ -public class InfiniteScrollAdapter extends RecyclerView.Adapter { +public class InfiniteScrollAdapter extends RecyclerView.Adapter + implements DiscreteScrollLayoutManager.InitialPositionProvider { - private static final int NOT_INITIALIZED = -1; + private static final int CENTER = Integer.MAX_VALUE / 2; private static final int RESET_BOUND = 100; public static InfiniteScrollAdapter wrap( @@ -23,19 +24,16 @@ public static InfiniteScrollAdapter wrap( private RecyclerView.Adapter wrapped; private DiscreteScrollLayoutManager layoutManager; - private int currentRangeStart; - public InfiniteScrollAdapter(@NonNull RecyclerView.Adapter wrapped) { this.wrapped = wrapped; this.wrapped.registerAdapterDataObserver(new DataSetChangeDelegate()); } @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { wrapped.onAttachedToRecyclerView(recyclerView); if (recyclerView instanceof DiscreteScrollView) { layoutManager = (DiscreteScrollLayoutManager) recyclerView.getLayoutManager(); - currentRangeStart = NOT_INITIALIZED; } else { String msg = recyclerView.getContext().getString(R.string.dsv_ex_msg_adapter_wrong_recycler); throw new RuntimeException(msg); @@ -43,21 +41,23 @@ public void onAttachedToRecyclerView(RecyclerView recyclerView) { } @Override - public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { wrapped.onDetachedFromRecyclerView(recyclerView); layoutManager = null; } @Override - public T onCreateViewHolder(ViewGroup parent, int viewType) { - if (currentRangeStart == NOT_INITIALIZED) { - resetRange(0); - } + public @NonNull T onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return wrapped.onCreateViewHolder(parent, viewType); } @Override - public void onBindViewHolder(T holder, int position) { + 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)); } @@ -68,7 +68,7 @@ public int getItemViewType(int position) { @Override public int getItemCount() { - return wrapped.getItemCount() <= 1 ? wrapped.getItemCount() : Integer.MAX_VALUE; + return isInfinite() ? Integer.MAX_VALUE : wrapped.getItemCount(); } public int getRealItemCount() { @@ -84,61 +84,69 @@ public int getRealPosition(int position) { } public int getClosestPosition(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())); - } - int adapterTarget = currentRangeStart + position; + ensureValidPosition(position); int adapterCurrent = layoutManager.getCurrentPosition(); - if (adapterTarget == adapterCurrent) { + int current = mapPositionToReal(adapterCurrent); + if (position == current) { return adapterCurrent; - } else if (adapterTarget < adapterCurrent) { - int adapterTargetNextSet = currentRangeStart + wrapped.getItemCount() + position; - return adapterCurrent - adapterTarget < adapterTargetNextSet - adapterCurrent ? - adapterTarget : adapterTargetNextSet; + } + 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 { - int adapterTargetPrevSet = currentRangeStart - wrapped.getItemCount() + position; - return adapterCurrent - adapterTargetPrevSet < adapterTarget - adapterCurrent ? - adapterTargetPrevSet : adapterTarget; + return distance < wraparoundDistance ? target : wraparoundTarget; } } private int mapPositionToReal(int position) { - int newPosition = position - currentRangeStart; - if (newPosition >= wrapped.getItemCount()) { - currentRangeStart += wrapped.getItemCount(); - if (Integer.MAX_VALUE - currentRangeStart <= RESET_BOUND) { - resetRange(0); - } - return 0; - } else if (newPosition < 0) { - currentRangeStart -= wrapped.getItemCount(); - if (currentRangeStart <= RESET_BOUND) { - resetRange(wrapped.getItemCount() - 1); - } - return wrapped.getItemCount() - 1; + if (position < CENTER) { + int rem = (CENTER - position) % wrapped.getItemCount(); + return rem == 0 ? 0 : wrapped.getItemCount() - rem; } else { - return newPosition; + return (position - CENTER) % wrapped.getItemCount(); } } - private void resetRange(int newPosition) { - if (getItemCount() == 1) { - currentRangeStart = 0; - layoutManager.scrollToPosition(0); - } else { - currentRangeStart = Integer.MAX_VALUE / 2; - layoutManager.scrollToPosition(currentRangeStart + newPosition); + 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() { - resetRange(0); + setPosition(getInitialPosition()); notifyDataSetChanged(); } From d3e6858c8202d46780f4c2bd97b8b20329cb8eec Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Mon, 30 Jul 2018 15:45:19 +0300 Subject: [PATCH 51/61] Published 1.4.9 --- README.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bea6f10..a8f3e3e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is similar to a ViewPager, but you can quickly and painlessly create layout, ## Gradle Add this into your dependencies block. ``` -compile 'com.yarolegovich:discrete-scrollview:1.4.8' +compile 'com.yarolegovich:discrete-scrollview:1.4.9' ``` ## Reporting an issue diff --git a/build.gradle b/build.gradle index 212e4c6..0519ee4 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' - publishVersion = '1.4.8' + publishVersion = '1.4.9' licences = ['Apache-2.0'] compileSdkVersion = 27 From a96b065131dd488e39d6449542978f6ba68422c7 Mon Sep 17 00:00:00 2001 From: yarolegovich Date: Sat, 1 Aug 2020 15:45:24 +0300 Subject: [PATCH 52/61] Migrated to androidx re #134 --- build.gradle | 32 +++++++++++++---- gradle.properties | 18 ++-------- gradle/wrapper/gradle-wrapper.properties | 4 +-- library/build.gradle | 34 +++++++------------ library/src/androidTest/AndroidManifest.xml | 6 ++-- .../DataSetModificationTest.java | 17 +++++----- .../DiscreteScrollViewTest.java | 17 ++++------ .../ScrollFunctionalityTest.java | 4 +-- .../context/TestActivity.java | 19 +++++------ .../context/TestAdapter.java | 13 +++---- .../discretescrollview/context/TestData.java | 6 ++-- .../custom/CustomAssertions.java | 10 +++--- .../DiscreteScrollLayoutManager.java | 13 +++---- .../DiscreteScrollView.java | 9 ++--- .../InfiniteScrollAdapter.java | 5 +-- .../discretescrollview/RecyclerViewProxy.java | 6 ++-- .../discretescrollview/transform/Pivot.java | 3 +- .../transform/ScaleTransformer.java | 3 +- .../util/ScrollListenerAdapter.java | 7 ++-- .../DiscreteScrollLayoutManagerTest.java | 3 +- .../stub/StubRecyclerViewProxy.java | 5 +-- sample/build.gradle | 11 +++--- .../sample/DiscreteScrollViewOptions.java | 30 +++++++++------- .../sample/MainActivity.java | 16 ++++----- .../sample/gallery/GalleryActivity.java | 10 +++--- .../sample/gallery/GalleryAdapter.java | 9 +++-- .../sample/shop/ShopActivity.java | 20 +++++------ .../sample/shop/ShopAdapter.java | 7 ++-- .../sample/weather/ForecastAdapter.java | 22 +++++++----- .../sample/weather/ForecastView.java | 10 ++---- .../sample/weather/WeatherActivity.java | 12 ++++--- .../src/main/res/layout/activity_gallery.xml | 6 ++-- sample/src/main/res/layout/activity_main.xml | 4 +-- sample/src/main/res/layout/activity_shop.xml | 8 +++-- .../src/main/res/layout/activity_weather.xml | 1 + .../res/layout/dialog_transition_time.xml | 1 + sample/src/main/res/layout/item_city_card.xml | 8 +++-- sample/src/main/res/layout/item_gallery.xml | 8 +++-- sample/src/main/res/layout/item_shop_card.xml | 8 +++-- sample/src/main/res/layout/toolbar.xml | 2 +- sample/src/main/res/layout/view_forecast.xml | 5 +-- sample/src/main/res/values/strings.xml | 4 +++ 42 files changed, 231 insertions(+), 205 deletions(-) diff --git a/build.gradle b/build.gradle index 0519ee4..0a596a1 100644 --- a/build.gradle +++ b/build.gradle @@ -4,8 +4,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' - classpath 'com.novoda:bintray-release:0.8.0' + classpath 'com.android.tools.build:gradle:4.0.1' } } @@ -13,6 +12,7 @@ allprojects { repositories { jcenter() maven { url "https://maven.google.com" } + maven { url "https://jitpack.io" } google() } } @@ -25,13 +25,31 @@ ext { userOrg = 'yarolegovich' groupId = 'com.yarolegovich' uploadName = 'DiscreteScrollView' - description = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' + desc = 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.' publishVersion = '1.4.9' licences = ['Apache-2.0'] - compileSdkVersion = 27 - buildToolsVersion = '26.0.2' - targetSdkVersion = 27 + compileSdkVersion = 29 + buildToolsVersion = '29.0.2' + targetSdkVersion = 29 - supportLibVersion = '27.1.1' + deps = [ + recycler : 'androidx.recyclerview:recyclerview:1.0.0', + designSupport : 'com.google.android.material:material:1.0.0', + annotations : 'androidx.annotation:annotation:1.1.0', + androidxCompat: 'androidx.appcompat:appcompat:1.1.0', + glide : 'com.github.bumptech.glide:glide:4.11.0', + materialPrefs : 'com.yarolegovich:mp:1.1.6' + ] + + testDeps = [ + hamcrest : 'org.hamcrest:hamcrest-library:1.3', + mockito : 'org.mockito:mockito-core:2.13.0', + jUnit : 'junit:junit:4.13', + robolectric : 'org.robolectric:robolectric:3.0', + espresso : 'androidx.test.espresso:espresso-core:3.1.0', + androidJUnit: 'androidx.test.ext:junit:1.1.1', + testRules : 'androidx.test:rules:1.1.1', + testRunner : 'androidx.test:runner:1.1.1' + ] } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index aac7c9b..a28ad2d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,17 +1,3 @@ -# Project-wide Gradle settings. +android.useAndroidX=true -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true +org.gradle.jvmargs=-Xmx1536m \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b99b98f..b670cfc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Feb 03 15:44:30 EET 2018 +#Thu Jul 30 09:08:49 EEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/library/build.gradle b/library/build.gradle index 99d325c..ee71a80 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'com.android.library' -apply plugin: 'com.novoda.bintray-release' android { compileSdkVersion rootProject.compileSdkVersion @@ -11,30 +10,23 @@ android { versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } } dependencies { - implementation "com.android.support:appcompat-v7:$supportLibVersion" - implementation "com.android.support:recyclerview-v7:$supportLibVersion" + implementation deps.recycler + implementation deps.annotations - 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' + testImplementation testDeps.robolectric + testImplementation testDeps.jUnit + testImplementation testDeps.mockito + testImplementation testDeps.hamcrest - androidTestImplementation "com.android.support:appcompat-v7:$supportLibVersion" - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' - androidTestImplementation 'org.hamcrest:hamcrest-library:1.3' -} - -publish { - artifactId = 'discrete-scrollview' - userOrg = rootProject.userOrg - groupId = rootProject.groupId - uploadName = rootProject.uploadName - publishVersion = rootProject.publishVersion - description = rootProject.description - licences = rootProject.licences + debugImplementation deps.androidxCompat + androidTestImplementation testDeps.espresso + androidTestImplementation testDeps.androidJUnit + androidTestImplementation testDeps.testRunner + androidTestImplementation testDeps.testRules + androidTestImplementation testDeps.hamcrest } \ No newline at end of file diff --git a/library/src/androidTest/AndroidManifest.xml b/library/src/androidTest/AndroidManifest.xml index 403a802..0d29d87 100644 --- a/library/src/androidTest/AndroidManifest.xml +++ b/library/src/androidTest/AndroidManifest.xml @@ -1,13 +1,13 @@ + package="com.yarolegovich.discretescrollview"> - + - + diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java index 8640f10..cf22b23 100644 --- a/library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java @@ -1,6 +1,7 @@ package com.yarolegovich.discretescrollview; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.yarolegovich.discretescrollview.context.TestData; @@ -10,10 +11,12 @@ import java.util.ArrayList; import java.util.List; -import static android.support.test.espresso.matcher.ViewMatchers.*; +import static androidx.test.espresso.matcher.ViewMatchers.assertThat; import static com.yarolegovich.discretescrollview.custom.CustomAssertions.currentPositionIs; import static com.yarolegovich.discretescrollview.custom.CustomAssertions.doesNotHaveChildren; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; /** * Created by yarolegovich on 2/3/18. @@ -214,9 +217,7 @@ public void notifyDataSetChanged_currentItemGoesOutsideItemRange_currentIsClampe @Override public void run() { List data = adapter.getData(); - for (int i = numOfItemsToRemove - 1; i >= 0; i--) { - data.remove(i); - } + data.subList(0, numOfItemsToRemove).clear(); adapter.notifyDataSetChanged(); } }); @@ -264,8 +265,8 @@ public void notifyDataSetChanged_scrollToPositionCalledAfterItemsRemoved_positio public void run() { List data = adapter.getData(); final int itemsToRemove = data.size() / 2; - for (int i = 0; i < itemsToRemove; i++) { - data.remove(0); + if (itemsToRemove > 0) { + data.subList(0, itemsToRemove).clear(); } adapter.notifyDataSetChanged(); scrollView.scrollToPosition(targetPosition); diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java index 5a34111..5c12209 100644 --- a/library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java @@ -1,16 +1,14 @@ package com.yarolegovich.discretescrollview; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.CallSuper; -import android.support.test.espresso.Espresso; -import android.support.test.espresso.IdlingRegistry; -import android.support.test.espresso.IdlingResource; -import android.support.test.espresso.ViewInteraction; -import android.support.test.espresso.idling.CountingIdlingResource; -import android.support.test.rule.ActivityTestRule; import android.view.View; +import androidx.annotation.CallSuper; +import androidx.test.espresso.Espresso; +import androidx.test.espresso.IdlingRegistry; +import androidx.test.espresso.IdlingResource; +import androidx.test.espresso.ViewInteraction; +import androidx.test.rule.ActivityTestRule; + import com.yarolegovich.discretescrollview.context.TestActivity; import com.yarolegovich.discretescrollview.context.TestAdapter; @@ -22,7 +20,6 @@ import java.util.List; import static com.yarolegovich.discretescrollview.custom.CustomAssertions.currentPositionIs; -import static org.junit.Assert.assertThat; /** * Created by yarolegovich on 2/3/18. diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java index 1ff4561..4cadeda 100644 --- a/library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java @@ -1,11 +1,11 @@ package com.yarolegovich.discretescrollview; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; -import static android.support.test.espresso.matcher.ViewMatchers.assertThat; +import static androidx.test.espresso.matcher.ViewMatchers.assertThat; import static com.yarolegovich.discretescrollview.custom.CustomAssertions.currentPositionIs; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java index cb1eacb..aa517f5 100644 --- a/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java @@ -1,26 +1,23 @@ package com.yarolegovich.discretescrollview.context; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.test.espresso.IdlingResource; -import android.support.test.espresso.idling.CountingIdlingResource; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.view.Gravity; import android.view.ViewGroup; import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.RecyclerView; +import androidx.test.espresso.IdlingResource; +import androidx.test.espresso.idling.CountingIdlingResource; + import com.yarolegovich.discretescrollview.DiscreteScrollView; import com.yarolegovich.discretescrollview.R; -import com.yarolegovich.discretescrollview.transform.ScaleTransformer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; @@ -29,7 +26,7 @@ * Created by yarolegovich on 2/4/18. */ -public class TestActivity extends AppCompatActivity implements DiscreteScrollView.ScrollStateChangeListener { +public class TestActivity extends AppCompatActivity implements DiscreteScrollView.ScrollStateChangeListener { private DiscreteScrollView scrollView; private TestAdapter adapter; diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java index 8db2f28..26665fa 100644 --- a/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java @@ -1,17 +1,13 @@ package com.yarolegovich.discretescrollview.context; -import android.content.Context; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.support.v7.widget.RecyclerView; -import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import java.util.List; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import java.util.List; /** * Created by yarolegovich on 2/4/18. @@ -27,11 +23,12 @@ public TestAdapter(List data) { } @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); this.recyclerView = recyclerView; } + @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { float dp = parent.getResources().getDisplayMetrics().density; diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java index b04686e..446eaf5 100644 --- a/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java @@ -3,7 +3,8 @@ import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.support.annotation.ColorInt; + +import androidx.annotation.ColorInt; import java.util.Random; @@ -23,7 +24,8 @@ public TestData() { image = new ColorDrawable(generateRandomColor()); } - private static @ColorInt int generateRandomColor() { + private static @ColorInt + int generateRandomColor() { return Color.argb(255, random.nextInt(256), random.nextInt(256), diff --git a/library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java index 8e65e31..99af895 100644 --- a/library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java +++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java @@ -1,16 +1,16 @@ package com.yarolegovich.discretescrollview.custom; -import android.support.test.espresso.NoMatchingViewException; -import android.support.test.espresso.ViewAssertion; -import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.view.View; import android.view.ViewGroup; +import androidx.recyclerview.widget.RecyclerView; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.espresso.ViewAssertion; + import com.yarolegovich.discretescrollview.DiscreteScrollView; import static org.hamcrest.Matchers.*; -import static android.support.test.espresso.matcher.ViewMatchers.*; +import static androidx.test.espresso.matcher.ViewMatchers.*; /** diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java index be81625..da60605 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java @@ -5,17 +5,18 @@ import android.graphics.PointF; import android.os.Bundle; import android.os.Parcelable; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.view.accessibility.AccessibilityEventCompat; -import android.support.v4.view.accessibility.AccessibilityRecordCompat; -import android.support.v7.widget.LinearSmoothScroller; -import android.support.v7.widget.RecyclerView; 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; diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java index 2e09c97..e41c2bb 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java @@ -2,13 +2,14 @@ import android.content.Context; import android.content.res.TypedArray; -import android.support.annotation.IntRange; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.RecyclerView; 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; diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java index dacf70c..803efd5 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java @@ -1,9 +1,10 @@ package com.yarolegovich.discretescrollview; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + import java.util.Locale; /** diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java b/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java index 32d5309..7aac486 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java @@ -1,11 +1,11 @@ package com.yarolegovich.discretescrollview; -import android.support.annotation.NonNull; -import android.support.v7.widget.LinearSmoothScroller; -import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + /** * Created by yarolegovich on 10/25/17. */ diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/transform/Pivot.java b/library/src/main/java/com/yarolegovich/discretescrollview/transform/Pivot.java index a97f8ff..ff2ae5e 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/transform/Pivot.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/transform/Pivot.java @@ -1,8 +1,9 @@ package com.yarolegovich.discretescrollview.transform; -import android.support.annotation.IntDef; import android.view.View; +import androidx.annotation.IntDef; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java b/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java index a42c313..2aa7069 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java @@ -1,8 +1,9 @@ package com.yarolegovich.discretescrollview.transform; -import android.support.annotation.FloatRange; import android.view.View; +import androidx.annotation.FloatRange; + /** * Created by yarolegovich on 03.03.2017. */ diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java b/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java index f47d7df..19b175c 100644 --- a/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java +++ b/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java @@ -1,8 +1,9 @@ package com.yarolegovich.discretescrollview.util; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.RecyclerView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; import com.yarolegovich.discretescrollview.DiscreteScrollView; diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java b/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java index f60ddcc..e11ba43 100644 --- a/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java +++ b/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java @@ -1,8 +1,9 @@ package com.yarolegovich.discretescrollview; -import android.support.v7.widget.RecyclerView; import android.view.View; +import androidx.recyclerview.widget.RecyclerView; + import com.yarolegovich.discretescrollview.stub.StubRecyclerViewProxy; import org.junit.Before; diff --git a/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java b/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java index bb5c3ca..eb72168 100644 --- a/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java +++ b/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java @@ -1,9 +1,10 @@ package com.yarolegovich.discretescrollview.stub; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; import android.view.View; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + import com.yarolegovich.discretescrollview.RecyclerViewProxy; import java.util.ArrayList; diff --git a/sample/build.gradle b/sample/build.gradle index 25e1b00..f684808 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -21,13 +21,10 @@ android { } dependencies { - implementation "com.android.support:appcompat-v7:$supportLibVersion" - implementation "com.android.support:cardview-v7:$supportLibVersion" - implementation "com.android.support:design:$supportLibVersion" - - implementation 'com.github.bumptech.glide:glide:3.7.0' - - implementation 'com.yarolegovich:mp:1.0.9' + implementation deps.designSupport + implementation deps.annotations + implementation deps.glide + implementation deps.materialPrefs implementation project(':library') } diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java index c693801..f8d7551 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java @@ -4,13 +4,14 @@ import android.content.DialogInterface; import android.content.SharedPreferences; import android.preference.PreferenceManager; -import android.support.design.widget.BottomSheetDialog; -import android.support.v7.widget.PopupMenu; -import android.support.v7.widget.RecyclerView; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import androidx.appcompat.widget.PopupMenu; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.bottomsheet.BottomSheetDialog; import com.yarolegovich.discretescrollview.DiscreteScrollView; import com.yarolegovich.discretescrollview.InfiniteScrollAdapter; @@ -45,22 +46,25 @@ public void onDismiss(DialogInterface dialog) { defaultPrefs().unregisterOnSharedPreferenceChangeListener(timeChangeListener); } }); - bsd.findViewById(R.id.dialog_btn_dismiss).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - bsd.dismiss(); - } - }); + View dismissBtn = bsd.findViewById(R.id.dialog_btn_dismiss); + if (dismissBtn != null) { + dismissBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + bsd.dismiss(); + } + }); + } bsd.show(); } public static void smoothScrollToUserSelectedPosition(final DiscreteScrollView scrollView, View anchor) { PopupMenu popupMenu = new PopupMenu(scrollView.getContext(), anchor); Menu menu = popupMenu.getMenu(); - final RecyclerView.Adapter adapter = scrollView.getAdapter(); + final RecyclerView.Adapter adapter = scrollView.getAdapter(); int itemCount = (adapter instanceof InfiniteScrollAdapter) ? - ((InfiniteScrollAdapter) adapter).getRealItemCount() : - adapter.getItemCount(); + ((InfiniteScrollAdapter) adapter).getRealItemCount() : + (adapter != null ? adapter.getItemCount() : 0); for (int i = 0; i < itemCount; i++) { menu.add(String.valueOf(i + 1)); } @@ -69,7 +73,7 @@ public static void smoothScrollToUserSelectedPosition(final DiscreteScrollView s public boolean onMenuItemClick(MenuItem item) { int destination = Integer.parseInt(String.valueOf(item.getTitle())) - 1; if (adapter instanceof InfiniteScrollAdapter) { - destination = ((InfiniteScrollAdapter) adapter).getClosestPosition(destination); + destination = ((InfiniteScrollAdapter) adapter).getClosestPosition(destination); } scrollView.smoothScrollToPosition(destination); return true; diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/MainActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/MainActivity.java index 262a68e..b025f10 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/MainActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/MainActivity.java @@ -4,13 +4,14 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import com.google.android.material.snackbar.Snackbar; import com.yarolegovich.discretescrollview.sample.gallery.GalleryActivity; import com.yarolegovich.discretescrollview.sample.shop.ShopActivity; import com.yarolegovich.discretescrollview.sample.weather.WeatherActivity; @@ -32,7 +33,7 @@ protected void onCreate(Bundle savedInstanceState) { root = findViewById(R.id.screen); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); findViewById(R.id.preview_shop).setOnClickListener(this); @@ -52,10 +53,9 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.mi_github: - open(URL_APP_REPO); - return true; + if (item.getItemId() == R.id.mi_github) { + open(URL_APP_REPO); + return true; } return super.onOptionsItemSelected(item); } diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java index 4decd15..abc8b32 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java @@ -2,13 +2,13 @@ import android.animation.ArgbEvaluator; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AppCompatActivity; import android.view.View; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import com.google.android.material.snackbar.Snackbar; import com.yarolegovich.discretescrollview.DiscreteScrollView; import com.yarolegovich.discretescrollview.sample.R; diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryAdapter.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryAdapter.java index a502361..f663638 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryAdapter.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryAdapter.java @@ -2,13 +2,15 @@ import android.app.Activity; import android.graphics.Point; -import android.support.annotation.ColorInt; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + import com.bumptech.glide.Glide; import com.yarolegovich.discretescrollview.sample.R; @@ -28,7 +30,7 @@ public GalleryAdapter(List data) { } @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); Activity context = (Activity) recyclerView.getContext(); Point windowDimensions = new Point(); @@ -36,6 +38,7 @@ public void onAttachedToRecyclerView(RecyclerView recyclerView) { itemHeight = Math.round(windowDimensions.y * 0.6f); } + @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java index af450a9..8557997 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java @@ -1,18 +1,18 @@ package com.yarolegovich.discretescrollview.sample.shop; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import com.google.android.material.snackbar.Snackbar; +import com.yarolegovich.discretescrollview.DSVOrientation; import com.yarolegovich.discretescrollview.DiscreteScrollView; import com.yarolegovich.discretescrollview.InfiniteScrollAdapter; -import com.yarolegovich.discretescrollview.DSVOrientation; import com.yarolegovich.discretescrollview.sample.DiscreteScrollViewOptions; import com.yarolegovich.discretescrollview.sample.R; import com.yarolegovich.discretescrollview.transform.ScaleTransformer; @@ -23,7 +23,7 @@ * Created by yarolegovich on 07.03.2017. */ -public class ShopActivity extends AppCompatActivity implements DiscreteScrollView.OnItemChangedListener, +public class ShopActivity extends AppCompatActivity implements DiscreteScrollView.OnItemChangedListener, View.OnClickListener { private List data; @@ -33,7 +33,7 @@ public class ShopActivity extends AppCompatActivity implements DiscreteScrollVie private TextView currentItemPrice; private ImageView rateItemButton; private DiscreteScrollView itemPicker; - private InfiniteScrollAdapter infiniteAdapter; + private InfiniteScrollAdapter infiniteAdapter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -108,8 +108,8 @@ private void changeRateButtonState(Item item) { } @Override - public void onCurrentItemChanged(@Nullable RecyclerView.ViewHolder viewHolder, int position) { - int positionInDataSet = infiniteAdapter.getRealPosition(position); + public void onCurrentItemChanged(@Nullable ShopAdapter.ViewHolder viewHolder, int adapterPosition) { + int positionInDataSet = infiniteAdapter.getRealPosition(adapterPosition); onItemChanged(data.get(positionInDataSet)); } diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopAdapter.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopAdapter.java index 21a3de1..a5cf1ff 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopAdapter.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopAdapter.java @@ -1,11 +1,13 @@ package com.yarolegovich.discretescrollview.sample.shop; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + import com.bumptech.glide.Glide; import com.yarolegovich.discretescrollview.sample.R; @@ -23,6 +25,7 @@ public ShopAdapter(List data) { this.data = data; } + @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); @@ -42,7 +45,7 @@ public int getItemCount() { return data.size(); } - class ViewHolder extends RecyclerView.ViewHolder { + static class ViewHolder extends RecyclerView.ViewHolder { private ImageView image; diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastAdapter.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastAdapter.java index 396b3e5..460d8bc 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastAdapter.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastAdapter.java @@ -1,16 +1,21 @@ package com.yarolegovich.discretescrollview.sample.weather; import android.graphics.Color; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; +import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + import com.bumptech.glide.Glide; -import com.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import com.yarolegovich.discretescrollview.sample.R; @@ -31,11 +36,12 @@ public ForecastAdapter(List data) { } @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); parentRecycler = recyclerView; } + @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); @@ -103,7 +109,7 @@ public void onClick(View v) { } } - private static class TintOnLoad implements RequestListener { + private static class TintOnLoad implements RequestListener { private ImageView imageView; private int tintColor; @@ -114,13 +120,13 @@ public TintOnLoad(ImageView view, int tintColor) { } @Override - public boolean onException(Exception e, Integer model, Target target, boolean isFirstResource) { + public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + imageView.setColorFilter(tintColor); return false; } @Override - public boolean onResourceReady(GlideDrawable resource, Integer model, Target target, boolean isFromMemoryCache, boolean isFirstResource) { - imageView.setColorFilter(tintColor); + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { return false; } } diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastView.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastView.java index 821c992..3921fa9 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastView.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastView.java @@ -6,9 +6,6 @@ import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Shader; -import android.os.Build; -import android.support.annotation.ArrayRes; -import android.support.annotation.RequiresApi; import android.util.AttributeSet; import android.view.Gravity; import android.view.animation.AccelerateDecelerateInterpolator; @@ -16,6 +13,8 @@ import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.ArrayRes; + import com.bumptech.glide.Glide; import com.yarolegovich.discretescrollview.sample.R; @@ -46,11 +45,6 @@ public ForecastView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public ForecastView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - { evaluator = new ArgbEvaluator(); diff --git a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java index 6f2cc4e..6543522 100644 --- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java +++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java @@ -1,11 +1,13 @@ package com.yarolegovich.discretescrollview.sample.weather; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.RecyclerView; + import com.yarolegovich.discretescrollview.DiscreteScrollView; import com.yarolegovich.discretescrollview.sample.R; import com.yarolegovich.discretescrollview.sample.DiscreteScrollViewOptions; @@ -74,7 +76,9 @@ public void onScroll( @Nullable ForecastAdapter.ViewHolder currentHolder, @Nullable ForecastAdapter.ViewHolder newHolder) { Forecast current = forecasts.get(currentIndex); - if (newIndex >= 0 && newIndex < cityPicker.getAdapter().getItemCount()) { + RecyclerView.Adapter adapter = cityPicker.getAdapter(); + int itemCount = adapter != null ? adapter.getItemCount() : 0; + if (newIndex >= 0 && newIndex < itemCount) { Forecast next = forecasts.get(newIndex); forecastView.onScroll(1f - Math.abs(position), current, next); } diff --git a/sample/src/main/res/layout/activity_gallery.xml b/sample/src/main/res/layout/activity_gallery.xml index 2ad6a6d..cbdb34d 100644 --- a/sample/src/main/res/layout/activity_gallery.xml +++ b/sample/src/main/res/layout/activity_gallery.xml @@ -1,5 +1,5 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index cb68fb1..f873a65 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -7,12 +7,12 @@ android:layout_height="match_parent" tools:context="com.yarolegovich.discretescrollview.sample.MainActivity"> - - + @@ -79,7 +81,7 @@ android:layout_width="16dp" android:layout_height="wrap_content" /> - @@ -118,7 +121,8 @@ android:layout_weight="1" android:text="@string/btn_smooth_scroll" android:textAllCaps="true" - android:textColor="@color/shopAccent" /> + android:textColor="@color/shopAccent" + tools:ignore="NestedWeights" />