diff --git a/README.md b/README.md
index e074fde..77748c8 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,15 @@ 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.5.1'
```
+
+## 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

@@ -42,10 +49,11 @@ scrollView.setAdapter(new YourAdapterImplementation());
```
### API
-#### Layout
+#### General
```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
+scrollView.setOverScrollEnabled(enabled); //Can also be set using android:overScrollMode xml attribute
```
#### Related to the current item:
```java
@@ -72,6 +80,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()
@@ -83,10 +95,51 @@ 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
+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.
+#### Disabling scroll
+It's possible to forbid user scroll in any or specific direction using:
+```java
+scrollView.setScrollConfig(config);
+```
+Where `config` is an instance of `DSVScrollConfig` enum. The default value enables scroll in any direction.
#### Callbacks
* Scroll state changes:
```java
-scrollView.setScrollStateChangeListener(listener);
+scrollView.addScrollStateChangeListener(listener);
+scrollView.removeScrollStateChangeListener(listener);
public interface ScrollStateChangeListener {
@@ -101,31 +154,36 @@ 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:
```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()
- 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:
```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);
}
```
diff --git a/build.gradle b/build.gradle
index f16ba00..66fd81e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,16 +1,20 @@
buildscript {
repositories {
jcenter()
+ google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.2.3'
- classpath 'com.novoda:bintray-release:0.4.0'
+ classpath 'com.android.tools.build:gradle:4.0.1'
+ classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
}
}
allprojects {
repositories {
jcenter()
+ maven { url "https://maven.google.com" }
+ maven { url "https://jitpack.io" }
+ google()
}
}
@@ -19,10 +23,27 @@ task clean(type: Delete) {
}
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.1.3'
- licences = ['Apache-2.0']
+ compileSdkVersion = 29
+ buildToolsVersion = '29.0.2'
+ targetSdkVersion = 29
+
+ 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.jar b/gradle/wrapper/gradle-wrapper.jar
index 13372ae..16e75d4 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 04e285f..b670cfc 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Mon Dec 28 10:00:20 PST 2015
+#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-2.14.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
index 9d82f78..4453cce
--- a/gradlew
+++ b/gradlew
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/usr/bin/env sh
##############################################################################
##
@@ -6,12 +6,30 @@
##
##############################################################################
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# 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
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
diff --git a/library/build.gradle b/library/build.gradle
index 524a226..8daa89d 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -1,29 +1,33 @@
apply plugin: 'com.android.library'
-apply plugin: 'com.novoda.bintray-release'
+apply from: rootProject.file('release-bintray.gradle')
android {
- compileSdkVersion 25
- buildToolsVersion "25.0.2"
+ compileSdkVersion rootProject.compileSdkVersion
+ buildToolsVersion rootProject.buildToolsVersion
defaultConfig {
minSdkVersion 14
- targetSdkVersion 25
+ targetSdkVersion rootProject.targetSdkVersion
versionCode 1
versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
- compile 'com.android.support:appcompat-v7:25.2.0'
- compile 'com.android.support:recyclerview-v7:25.2.0'
-}
+ implementation deps.recycler
+ implementation deps.annotations
+
+ testImplementation testDeps.robolectric
+ testImplementation testDeps.jUnit
+ testImplementation testDeps.mockito
+ testImplementation testDeps.hamcrest
-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
new file mode 100644
index 0000000..0d29d87
--- /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/DataSetModificationTest.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java
new file mode 100644
index 0000000..cf22b23
--- /dev/null
+++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java
@@ -0,0 +1,285 @@
+package com.yarolegovich.discretescrollview;
+
+
+import androidx.test.ext.junit.runners.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 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.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * 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();
+ data.subList(0, numOfItemsToRemove).clear();
+ 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;
+ if (itemsToRemove > 0) {
+ data.subList(0, itemsToRemove).clear();
+ }
+ 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;
+ }
+}
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..5c12209
--- /dev/null
+++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java
@@ -0,0 +1,82 @@
+package com.yarolegovich.discretescrollview;
+
+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;
+
+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;
+
+/**
+ * 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/ScrollFunctionalityTest.java b/library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java
new file mode 100644
index 0000000..4cadeda
--- /dev/null
+++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java
@@ -0,0 +1,110 @@
+package com.yarolegovich.discretescrollview;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+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;
+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));
+ }
+}
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..aa517f5
--- /dev/null
+++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java
@@ -0,0 +1,108 @@
+package com.yarolegovich.discretescrollview.context;
+
+import android.os.Bundle;
+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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+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..26665fa
--- /dev/null
+++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java
@@ -0,0 +1,71 @@
+package com.yarolegovich.discretescrollview.context;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+/**
+ * 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(@NonNull RecyclerView recyclerView) {
+ super.onAttachedToRecyclerView(recyclerView);
+ this.recyclerView = recyclerView;
+ }
+
+ @NonNull
+ @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..446eaf5
--- /dev/null
+++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java
@@ -0,0 +1,34 @@
+package com.yarolegovich.discretescrollview.context;
+
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+
+import androidx.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));
+ }
+}
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..99af895
--- /dev/null
+++ b/library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java
@@ -0,0 +1,73 @@
+package com.yarolegovich.discretescrollview.custom;
+
+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 androidx.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;
+ }
+ }
+}
diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java b/library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java
similarity index 90%
rename from library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java
rename to library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java
index 9a5b3e8..8f7d7dc 100644
--- a/library/src/main/java/com/yarolegovich/discretescrollview/Orientation.java
+++ b/library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java
@@ -1,13 +1,12 @@
package com.yarolegovich.discretescrollview;
import android.graphics.Point;
-import android.support.v7.widget.RecyclerView;
import android.view.View;
/**
* Created by yarolegovich on 16.03.2017.
*/
-public enum Orientation {
+public enum DSVOrientation {
HORIZONTAL {
@Override
@@ -41,9 +40,9 @@ 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);
+ float getDistanceFromCenter(Point center, float viewCenterX, float viewCenterY);
boolean isViewVisible(Point center, int halfWidth, int halfHeight, int endBound, int extraSpace);
@@ -100,12 +99,12 @@ 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
- public float getDistanceFromCenter(Point center, int viewCenterX, int viewCenterY) {
+ public float getDistanceFromCenter(Point center, float viewCenterX, float viewCenterY) {
return viewCenterX - center.x;
}
@@ -161,12 +160,12 @@ 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
- public float getDistanceFromCenter(Point center, int viewCenterX, int viewCenterY) {
+ public float getDistanceFromCenter(Point center, float viewCenterX, float viewCenterY) {
return viewCenterY - center.y;
}
@@ -217,5 +216,4 @@ public int getPendingDy(int pendingScroll) {
}
}
- ;
}
diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DSVScrollConfig.java b/library/src/main/java/com/yarolegovich/discretescrollview/DSVScrollConfig.java
new file mode 100644
index 0000000..f6c743b
--- /dev/null
+++ b/library/src/main/java/com/yarolegovich/discretescrollview/DSVScrollConfig.java
@@ -0,0 +1,30 @@
+package com.yarolegovich.discretescrollview;
+
+public enum DSVScrollConfig {
+ ENABLED {
+ @Override
+ boolean isScrollBlocked(Direction direction) {
+ return false;
+ }
+ },
+ FORWARD_ONLY {
+ @Override
+ boolean isScrollBlocked(Direction direction) {
+ return direction == Direction.START;
+ }
+ },
+ BACKWARD_ONLY {
+ @Override
+ boolean isScrollBlocked(Direction direction) {
+ return direction == Direction.END;
+ }
+ },
+ DISABLED {
+ @Override
+ boolean isScrollBlocked(Direction direction) {
+ return true;
+ }
+ };
+
+ abstract boolean isScrollBlocked(Direction direction);
+}
diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/Direction.java b/library/src/main/java/com/yarolegovich/discretescrollview/Direction.java
index 8617e6f..d3fcf49 100644
--- a/library/src/main/java/com/yarolegovich/discretescrollview/Direction.java
+++ b/library/src/main/java/com/yarolegovich/discretescrollview/Direction.java
@@ -10,16 +10,40 @@ enum Direction {
public int applyTo(int delta) {
return delta * -1;
}
+
+ @Override
+ public boolean sameAs(int direction) {
+ return direction < 0;
+ }
+
+ @Override
+ public Direction reverse() {
+ return Direction.END;
+ }
},
END {
@Override
public int applyTo(int delta) {
return delta;
}
+
+ @Override
+ public boolean sameAs(int direction) {
+ return direction > 0;
+ }
+
+ @Override
+ public Direction reverse() {
+ return Direction.START;
+ }
};
public abstract int applyTo(int delta);
+ public abstract boolean sameAs(int direction);
+
+ public abstract Direction reverse();
+
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..c22fba6 100644
--- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java
+++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManager.java
@@ -5,109 +5,152 @@
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.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
+
import com.yarolegovich.discretescrollview.transform.DiscreteScrollItemTransformer;
+import java.util.Locale;
+
/**
* Created by yarolegovich on 17.02.2017.
*/
-class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager {
+public class DiscreteScrollLayoutManager extends RecyclerView.LayoutManager {
+
+ static final int NO_POSITION = -1;
private static final String EXTRA_POSITION = "extra_position";
+ private static final int DEFAULT_TIME_FOR_ITEM_SETTLE = 300;
+ private static final int DEFAULT_FLING_THRESHOLD = 2100; //Decrease to increase sensitivity.
+ private static final int DEFAULT_TRANSFORM_CLAMP_ITEM_COUNT = 1;
- private static final int NO_POSITION = -1;
- private static final int DEFAULT_TIME_FOR_ITEM_SETTLE = 150;
+ 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 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;
+ private DSVOrientation.Helper orientationHelper;
+
+ protected boolean isFirstOrEmptyLayout;
private Context context;
private int timeForItemSettle;
private int offscreenItems;
+ private int transformClampItemCount;
+
+ private boolean dataSetChangeShiftedPosition;
+
+ private int flingThreshold;
+ private boolean shouldSlideOnFling;
+
+ private int viewWidth, viewHeight;
- private SparseArray detachedCache;
+ @NonNull
+ private DSVScrollConfig scrollConfig = DSVScrollConfig.ENABLED;
@NonNull
private final ScrollStateListener scrollStateListener;
private DiscreteScrollItemTransformer itemTransformer;
+ private RecyclerViewProxy recyclerViewProxy;
+
public DiscreteScrollLayoutManager(
- Context c,
+ @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;
this.currentPosition = NO_POSITION;
+ this.flingThreshold = DEFAULT_FLING_THRESHOLD;
+ this.shouldSlideOnFling = false;
this.recyclerCenter = new Point();
this.currentViewCenter = new Point();
this.viewCenterIterator = new Point();
this.detachedCache = new SparseArray<>();
this.scrollStateListener = scrollStateListener;
this.orientationHelper = orientation.createHelper();
- setAutoMeasureEnabled(true);
+ this.recyclerViewProxy = new RecyclerViewProxy(this);
+ this.transformClampItemCount = DEFAULT_TRANSFORM_CLAMP_ITEM_COUNT;
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.getItemCount() == 0) {
- removeAndRecycleAllViews(recycler);
+ recyclerViewProxy.removeAndRecycleAllViews(recycler);
currentPosition = pendingPosition = NO_POSITION;
scrolled = pendingScroll = 0;
return;
}
- boolean isFirstOrEmptyLayout = getChildCount() == 0;
- if (isFirstOrEmptyLayout) {
- initChildDimensions(recycler);
- }
+ ensureValidPosition(state);
- updateRecyclerDimensions();
+ updateRecyclerDimensions(state);
- detachAndScrapAttachedViews(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 = recyclerViewProxy.getChildCount() == 0;
+ if (isFirstOrEmptyLayout) {
+ initChildDimensions(recycler);
+ }
+ }
+
+ recyclerViewProxy.detachAndScrapAttachedViews(recycler);
fill(recycler);
applyItemTransformToChildren();
+ }
+ private void ensureValidPosition(RecyclerView.State state) {
+ if (currentPosition == NO_POSITION || currentPosition >= state.getItemCount()) {
+ //currentPosition might have been assigned in onRestoreInstanceState()
+ //which can lead to a crash (position out of bounds) when data set
+ //is not persisted across rotations
+ currentPosition = 0;
+ }
+ }
+
+ @Override
+ public void onLayoutCompleted(RecyclerView.State state) {
if (isFirstOrEmptyLayout) {
scrollStateListener.onCurrentViewFirstLayout();
+ isFirstOrEmptyLayout = false;
+ } else if (dataSetChangeShiftedPosition) {
+ scrollStateListener.onDataSetChangeChangedPosition();
+ dataSetChangeShiftedPosition = false;
}
}
- 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.getMeasuredWidthWithMargin(viewToMeasure);
+ int childViewHeight = recyclerViewProxy.getMeasuredHeightWithMargin(viewToMeasure);
childHalfWidth = childViewWidth / 2;
childHalfHeight = childViewHeight / 2;
@@ -118,19 +161,31 @@ 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(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);
}
- 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)) {
@@ -143,78 +198,102 @@ 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) {
final int positionStep = direction.applyTo(1);
+ //Predictive layout is required when we are doing smooth fast scroll towards pendingPosition
+ boolean noPredictiveLayoutRequired = pendingPosition == NO_POSITION
+ || !direction.sameAs(pendingPosition - currentPosition);
+
viewCenterIterator.set(currentViewCenter.x, currentViewCenter.y);
- for (int 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;
}
}
}
- 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();
}
@Override
- public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+ public void onItemsAdded(@NonNull 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, recyclerViewProxy.getItemCount() - 1);
}
+ onNewPosition(newPosition);
}
@Override
- public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
- if (getItemCount() == 0) {
- currentPosition = NO_POSITION;
+ public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart, int itemCount) {
+ int newPosition = currentPosition;
+ if (recyclerViewProxy.getItemCount() == 0) {
+ 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) {
+ public void onItemsChanged(@NonNull 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;
+ }
+
+ private void onNewPosition(int position) {
+ if (currentPosition != position) {
+ currentPosition = position;
+ dataSetChangeShiftedPosition = true;
+ }
}
@Override
@@ -227,8 +306,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;
}
@@ -244,7 +323,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);
@@ -257,11 +336,13 @@ 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);
- itemTransformer.transformItem(child, getCenterRelativePositionOf(child));
+ int clampAfterDistance = scrollToChangeCurrent * transformClampItemCount;
+ for (int i = 0; i < recyclerViewProxy.getChildCount(); i++) {
+ View child = recyclerViewProxy.getChildAt(i);
+ float position = getCenterRelativePositionOf(child, clampAfterDistance);
+ itemTransformer.transformItem(child, position);
}
}
}
@@ -273,22 +354,21 @@ public void scrollToPosition(int position) {
}
currentPosition = position;
- requestLayout();
+ recyclerViewProxy.requestLayout();
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
- if (currentPosition == position) {
+ 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();
+ checkTargetPosition(state, position);
+ if (currentPosition == NO_POSITION) {
+ //Layout not happened yet
+ currentPosition = position;
+ } else {
+ startSmoothPendingScroll(position);
+ }
}
@Override
@@ -372,16 +452,21 @@ private void onDragStart() {
pendingScroll = 0;
}
+ public boolean isFlingDisallowed(int velocityX, int velocityY) {
+ int velocity = orientationHelper.getFlingVelocity(velocityX, velocityY);
+ Direction direction = Direction.fromDelta(velocity);
+ return scrollConfig.isScrollBlocked(direction);
+ }
+
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(velocity / flingThreshold) : 1;
+ 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) {
- startSmoothPendingScroll();
- }
+ startSmoothPendingScroll(newPosition);
} else {
returnToCurrentPosition();
}
@@ -394,10 +479,15 @@ public void returnToCurrentPosition() {
}
}
- private int calculateAllowedScrollIn(Direction direction) {
+ protected int calculateAllowedScrollIn(Direction direction) {
if (pendingScroll != 0) {
return Math.abs(pendingScroll);
}
+ if (currentScrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
+ if (scrollConfig.isScrollBlocked(direction)) {
+ return direction.reverse().applyTo(scrolled);
+ }
+ }
int allowedScroll;
boolean isBoundReached;
boolean isScrollDirectionAsBefore = direction.applyTo(scrolled) > 0;
@@ -405,7 +495,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);
@@ -422,17 +512,86 @@ 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) {
+ if (currentPosition == position) return;
+ pendingScroll = -scrolled;
+ Direction direction = Direction.fromDelta(position - currentPosition);
+ int distanceToScroll = Math.abs(position - currentPosition) * scrollToChangeCurrent;
+ pendingScroll += direction.applyTo(distanceToScroll);
+ pendingPosition = position;
+ startSmoothPendingScroll();
+ }
+
+ @Override
+ public boolean isAutoMeasureEnabled() {
+ return true;
+ }
+
+ @Override
+ public int computeVerticalScrollRange(@NonNull RecyclerView.State state) {
+ return computeScrollRange(state);
+ }
+
+ @Override
+ public int computeVerticalScrollOffset(@NonNull RecyclerView.State state) {
+ return computeScrollOffset(state);
+ }
+
+ @Override
+ public int computeVerticalScrollExtent(@NonNull RecyclerView.State state) {
+ return computeScrollExtent(state);
+ }
+
+ @Override
+ public int computeHorizontalScrollRange(@NonNull RecyclerView.State state) {
+ return computeScrollRange(state);
+ }
+
+ @Override
+ public int computeHorizontalScrollOffset(@NonNull RecyclerView.State state) {
+ return computeScrollOffset(state);
+ }
+
+ @Override
+ public int computeHorizontalScrollExtent(@NonNull 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 (state.getItemCount() == 0) {
+ return 0;
+ } else {
+ return scrollToChangeCurrent * (state.getItemCount() - 1);
+ }
}
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
- if (newAdapter.getItemCount() > 0) {
- pendingPosition = NO_POSITION;
- scrolled = pendingScroll = 0;
+ pendingPosition = NO_POSITION;
+ scrolled = pendingScroll = 0;
+ if (newAdapter instanceof InitialPositionProvider) {
+ currentPosition = ((InitialPositionProvider) newAdapter).getInitialPosition();
+ } else {
currentPosition = 0;
}
- removeAllViews();
+ recyclerViewProxy.removeAllViews();
}
@Override
@@ -458,6 +617,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;
}
@@ -469,13 +638,30 @@ public void setTimeForItemSettle(int timeForItemSettle) {
public void setOffscreenItems(int offscreenItems) {
this.offscreenItems = offscreenItems;
extraLayoutSpace = scrollToChangeCurrent * offscreenItems;
- requestLayout();
+ recyclerViewProxy.requestLayout();
}
- public void setOrientation(Orientation orientation) {
+ public void setTransformClampItemCount(int transformClampItemCount) {
+ this.transformClampItemCount = transformClampItemCount;
+ applyItemTransformToChildren();
+ }
+
+ public void setOrientation(DSVOrientation orientation) {
orientationHelper = orientation.createHelper();
- removeAllViews();
- requestLayout();
+ recyclerViewProxy.removeAllViews();
+ recyclerViewProxy.requestLayout();
+ }
+
+ public void setShouldSlideOnFling(boolean result) {
+ shouldSlideOnFling = result;
+ }
+
+ public void setSlideOnFlingThreshold(int threshold) {
+ flingThreshold = threshold;
+ }
+
+ public void setScrollConfig(@NonNull DSVScrollConfig config) {
+ scrollConfig = config;
}
public int getCurrentPosition() {
@@ -483,20 +669,34 @@ public int getCurrentPosition() {
}
@Override
- public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
- if (getChildCount() > 0) {
- final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
- record.setFromIndex(getPosition(getFirstChild()));
- record.setToIndex(getPosition(getLastChild()));
+ if (recyclerViewProxy.getChildCount() > 0) {
+ event.setFromIndex(getPosition(getFirstChild()));
+ event.setToIndex(getPosition(getLastChild()));
}
}
- private float getCenterRelativePositionOf(View v) {
+ private float getCenterRelativePositionOf(View v, int maxDistance) {
+ float childCenterX = getDecoratedLeft(v) + v.getWidth() * 0.5f;
+ float childCenterY = getDecoratedTop(v) + v.getHeight() * 0.5f;
float distanceFromCenter = orientationHelper.getDistanceFromCenter(recyclerCenter,
- getDecoratedLeft(v) + childHalfWidth,
- getDecoratedTop(v) + childHalfHeight);
- return Math.min(Math.max(-1f, distanceFromCenter / scrollToChangeCurrent), 1f);
+ childCenterX,
+ childCenterY);
+ return Math.min(Math.max(-1f, distanceFromCenter / maxDistance), 1f);
+ }
+
+ private int checkNewOnFlingPositionIsInBounds(int position) {
+ final int itemCount = recyclerViewProxy.getItemCount();
+ //The check is required in case slide through multiple items is turned on
+ if (currentPosition != 0 && position < 0) {
+ //If currentPosition == 0 && position < 0 we forbid scroll to the left,
+ //but if currentPosition != 0 we can slide to the first item
+ return 0;
+ } else if (currentPosition != itemCount - 1 && position >= itemCount) {
+ return itemCount - 1;
+ }
+ return position;
}
private int getHowMuchIsLeftToScroll(int dx) {
@@ -504,15 +704,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() {
@@ -520,12 +720,15 @@ 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);
}
private boolean isInBounds(int itemPosition) {
- return itemPosition >= 0 && itemPosition < getItemCount();
+ return itemPosition >= 0 && itemPosition < recyclerViewProxy.getItemCount();
}
private boolean isViewVisible(Point viewCenter, int endBound) {
@@ -534,6 +737,22 @@ 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;
+ }
+
+ protected void setOrientationHelper(DSVOrientation.Helper orientationHelper) {
+ this.orientationHelper = orientationHelper;
+ }
+
private class DiscreteLinearSmoothScroller extends LinearSmoothScroller {
public DiscreteLinearSmoothScroller(Context context) {
@@ -575,6 +794,11 @@ public interface ScrollStateListener {
void onScroll(float currentViewPosition);
void onCurrentViewFirstLayout();
+
+ void onDataSetChangeChangedPosition();
}
+ public interface InitialPositionProvider {
+ int getInitialPosition();
+ }
}
diff --git a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java
index 209bbb1..560e5d0 100644
--- a/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java
+++ b/library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java
@@ -2,28 +2,42 @@
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;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Created by yarolegovich on 18.02.2017.
*/
-@SuppressWarnings("unchecked")
+@SuppressWarnings({"unchecked", "rawtypes"})
public class DiscreteScrollView extends RecyclerView {
- private static final int DEFAULT_ORIENTATION = Orientation.HORIZONTAL.ordinal();
+ public static final int NO_POSITION = DiscreteScrollLayoutManager.NO_POSITION;
+
+ private static final int DEFAULT_ORIENTATION = DSVOrientation.HORIZONTAL.ordinal();
private DiscreteScrollLayoutManager layoutManager;
- private ScrollStateChangeListener scrollStateChangeListener;
- private OnItemChangedListener onItemChangedListener;
+ private List scrollStateChangeListeners;
+ private List onItemChangedListeners;
+ private Runnable notifyItemChangedRunnable = new Runnable() {
+ @Override
+ public void run() {
+ notifyCurrentItemChanged();
+ }
+ };
+
+ private boolean isOverScrollEnabled;
public DiscreteScrollView(Context context) {
super(context);
@@ -41,6 +55,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);
@@ -48,9 +65,11 @@ private void init(AttributeSet attrs) {
ta.recycle();
}
+ isOverScrollEnabled = getOverScrollMode() != OVER_SCROLL_NEVER;
+
layoutManager = new DiscreteScrollLayoutManager(
getContext(), new ScrollStateListener(),
- Orientation.values()[orientation]);
+ DSVOrientation.values()[orientation]);
setLayoutManager(layoutManager);
}
@@ -66,6 +85,9 @@ public void setLayoutManager(LayoutManager layout) {
@Override
public boolean fling(int velocityX, int velocityY) {
+ if (layoutManager.isFlingDisallowed(velocityX, velocityY)) {
+ return false;
+ }
boolean isFling = super.fling(velocityX, velocityY);
if (isFling) {
layoutManager.onFling(velocityX, velocityY);
@@ -81,6 +103,15 @@ public ViewHolder getViewHolder(int position) {
return view != null ? getChildViewHolder(view) : null;
}
+ @Override
+ public void scrollToPosition(int position) {
+ int currentPosition = layoutManager.getCurrentPosition();
+ super.scrollToPosition(position);
+ if (currentPosition != position) {
+ notifyCurrentItemChanged();
+ }
+ }
+
/**
* @return adapter position of the current item or -1 if nothing is selected
*/
@@ -96,7 +127,15 @@ public void setItemTransitionTimeMillis(@IntRange(from = 10) int millis) {
layoutManager.setTimeForItemSettle(millis);
}
- public void setOrientation(Orientation orientation) {
+ public void setSlideOnFling(boolean result){
+ layoutManager.setShouldSlideOnFling(result);
+ }
+
+ public void setSlideOnFlingThreshold(int threshold){
+ layoutManager.setSlideOnFlingThreshold(threshold);
+ }
+
+ public void setOrientation(DSVOrientation orientation) {
layoutManager.setOrientation(orientation);
}
@@ -104,83 +143,146 @@ public void setOffscreenItems(int items) {
layoutManager.setOffscreenItems(items);
}
- public void setScrollStateChangeListener(ScrollStateChangeListener> scrollStateChangeListener) {
- this.scrollStateChangeListener = scrollStateChangeListener;
+ public void setScrollConfig(@NonNull DSVScrollConfig config) {
+ layoutManager.setScrollConfig(config);
+ }
+
+ public void setClampTransformProgressAfter(@IntRange(from = 1) int itemCount) {
+ if (itemCount <= 1) {
+ throw new IllegalArgumentException("must be >= 1");
+ }
+ layoutManager.setTransformClampItemCount(itemCount);
+ }
+
+ public void setOverScrollEnabled(boolean overScrollEnabled) {
+ isOverScrollEnabled = overScrollEnabled;
+ setOverScrollMode(OVER_SCROLL_NEVER);
+ }
+
+ public void addScrollStateChangeListener(@NonNull ScrollStateChangeListener> scrollStateChangeListener) {
+ scrollStateChangeListeners.add(scrollStateChangeListener);
+ }
+
+ public void addScrollListener(@NonNull ScrollListener> scrollListener) {
+ addScrollStateChangeListener(new ScrollListenerAdapter(scrollListener));
+ }
+
+ public void addOnItemChangedListener(@NonNull OnItemChangedListener> onItemChangedListener) {
+ onItemChangedListeners.add(onItemChangedListener);
+ }
+
+ public void removeScrollStateChangeListener(@NonNull ScrollStateChangeListener> scrollStateChangeListener) {
+ scrollStateChangeListeners.remove(scrollStateChangeListener);
+ }
+
+ public void removeScrollListener(@NonNull ScrollListener> scrollListener) {
+ removeScrollStateChangeListener(new ScrollListenerAdapter<>(scrollListener));
+ }
+
+ public void removeItemChangedListener(@NonNull OnItemChangedListener> onItemChangedListener) {
+ onItemChangedListeners.remove(onItemChangedListener);
}
- public void setScrollListener(ScrollListener> scrollListener) {
- setScrollStateChangeListener(new ScrollListenerAdapter(scrollListener));
+ private void notifyScrollStart(ViewHolder holder, int current) {
+ for (ScrollStateChangeListener listener : scrollStateChangeListeners) {
+ listener.onScrollStart(holder, current);
+ }
}
- public void setOnItemChangedListener(OnItemChangedListener> onItemChangedListener) {
- this.onItemChangedListener = onItemChangedListener;
+ private void notifyScrollEnd(ViewHolder holder, int current) {
+ for (ScrollStateChangeListener listener : scrollStateChangeListeners) {
+ listener.onScrollEnd(holder, current);
+ }
+ }
+
+ private void notifyScroll(float position,
+ int currentIndex, int newIndex,
+ ViewHolder currentHolder, ViewHolder newHolder) {
+ for (ScrollStateChangeListener listener : scrollStateChangeListeners) {
+ listener.onScroll(position, currentIndex, newIndex,
+ currentHolder,
+ newHolder);
+ }
+ }
+
+ private void notifyCurrentItemChanged(ViewHolder holder, int current) {
+ for (OnItemChangedListener listener : onItemChangedListeners) {
+ listener.onCurrentItemChanged(holder, current);
+ }
+ }
+
+ private void notifyCurrentItemChanged() {
+ removeCallbacks(notifyItemChangedRunnable);
+ if (onItemChangedListeners.isEmpty()) {
+ return;
+ }
+ int current = layoutManager.getCurrentPosition();
+ ViewHolder currentHolder = getViewHolder(current);
+ if (currentHolder == null) {
+ post(notifyItemChangedRunnable);
+ } else {
+ notifyCurrentItemChanged(currentHolder, current);
+ }
}
private class ScrollStateListener implements DiscreteScrollLayoutManager.ScrollStateListener {
@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
public void onScrollStart() {
- if (scrollStateChangeListener != null) {
- int current = layoutManager.getCurrentPosition();
- ViewHolder holder = getViewHolder(current);
- if (holder != null) {
- scrollStateChangeListener.onScrollStart(holder, current);
- }
+ removeCallbacks(notifyItemChangedRunnable);
+ 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());
-
- int newCurrent = current + (currentViewPosition < 0 ? 1 : -1);
- ViewHolder newCurrentHolder = getViewHolder(newCurrent);
-
- if (currentHolder != null && newCurrentHolder != null) {
- scrollStateChangeListener.onScroll(
- currentViewPosition, currentHolder,
- newCurrentHolder);
- }
+ if (scrollStateChangeListeners.isEmpty()) {
+ return;
+ }
+ int currentIndex = getCurrentItem();
+ int newIndex = layoutManager.getNextPosition();
+ if (currentIndex != newIndex) {
+ notifyScroll(currentViewPosition,
+ currentIndex, newIndex,
+ getViewHolder(currentIndex),
+ getViewHolder(newIndex));
}
}
@Override
public void onCurrentViewFirstLayout() {
- if (onItemChangedListener != null) {
- int current = layoutManager.getCurrentPosition();
- ViewHolder currentHolder = getViewHolder(current);
- if (currentHolder != null) {
- onItemChangedListener.onCurrentItemChanged(currentHolder, current);
- }
- }
+ notifyCurrentItemChanged();
+ }
+
+ @Override
+ public void onDataSetChangeChangedPosition() {
+ notifyCurrentItemChanged();
}
}
@@ -190,19 +292,26 @@ 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 {
/*
* This method will be also triggered when view appears on the screen for the first time.
+ * If data set is empty, viewHolder will be null and adapterPosition will be NO_POSITION
*/
- void onCurrentItemChanged(@NonNull T viewHolder, int adapterPosition);
-
+ void onCurrentItemChanged(@Nullable T viewHolder, int adapterPosition);
}
}
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..803efd5
--- /dev/null
+++ b/library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java
@@ -0,0 +1,179 @@
+package com.yarolegovich.discretescrollview;
+
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.Locale;
+
+/**
+ * Created by yarolegovich on 28-Apr-17.
+ */
+
+public class InfiniteScrollAdapter extends RecyclerView.Adapter
+ implements DiscreteScrollLayoutManager.InitialPositionProvider {
+
+ private static final int CENTER = Integer.MAX_VALUE / 2;
+ private static final int RESET_BOUND = 100;
+
+ public static InfiniteScrollAdapter wrap(
+ @NonNull RecyclerView.Adapter adapter) {
+ return new InfiniteScrollAdapter<>(adapter);
+ }
+
+ private RecyclerView.Adapter wrapped;
+ private DiscreteScrollLayoutManager layoutManager;
+
+ public InfiniteScrollAdapter(@NonNull RecyclerView.Adapter wrapped) {
+ this.wrapped = wrapped;
+ this.wrapped.registerAdapterDataObserver(new DataSetChangeDelegate());
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ wrapped.onAttachedToRecyclerView(recyclerView);
+ if (recyclerView instanceof DiscreteScrollView) {
+ layoutManager = (DiscreteScrollLayoutManager) recyclerView.getLayoutManager();
+ } else {
+ String msg = recyclerView.getContext().getString(R.string.dsv_ex_msg_adapter_wrong_recycler);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ wrapped.onDetachedFromRecyclerView(recyclerView);
+ layoutManager = null;
+ }
+
+ @Override
+ public @NonNull T onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return wrapped.onCreateViewHolder(parent, viewType);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull T holder, int position) {
+ if (isResetRequired(position)) {
+ int resetPosition = CENTER + mapPositionToReal(layoutManager.getCurrentPosition());
+ setPosition(resetPosition);
+ return;
+ }
+ wrapped.onBindViewHolder(holder, mapPositionToReal(position));
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return wrapped.getItemViewType(mapPositionToReal(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return isInfinite() ? Integer.MAX_VALUE : wrapped.getItemCount();
+ }
+
+ public int getRealItemCount() {
+ return wrapped.getItemCount();
+ }
+
+ public int getRealCurrentPosition() {
+ return getRealPosition(layoutManager.getCurrentPosition());
+ }
+
+ public int getRealPosition(int position) {
+ return mapPositionToReal(position);
+ }
+
+ public int getClosestPosition(int position) {
+ ensureValidPosition(position);
+ int adapterCurrent = layoutManager.getCurrentPosition();
+ int current = mapPositionToReal(adapterCurrent);
+ if (position == current) {
+ return adapterCurrent;
+ }
+ int delta = position - current;
+ int target = adapterCurrent + delta;
+ int wraparoundTarget = adapterCurrent + (position > current ?
+ delta - wrapped.getItemCount() :
+ wrapped.getItemCount() + delta);
+ int distance = Math.abs(adapterCurrent - target);
+ int wraparoundDistance = Math.abs(adapterCurrent - wraparoundTarget);
+ if (distance == wraparoundDistance) {
+ //Scroll to the right feels more natural, so prefer it
+ return target > adapterCurrent ? target : wraparoundTarget;
+ } else {
+ return distance < wraparoundDistance ? target : wraparoundTarget;
+ }
+ }
+
+ private int mapPositionToReal(int position) {
+ if (position < CENTER) {
+ int rem = (CENTER - position) % wrapped.getItemCount();
+ return rem == 0 ? 0 : wrapped.getItemCount() - rem;
+ } else {
+ return (position - CENTER) % wrapped.getItemCount();
+ }
+ }
+
+ private boolean isResetRequired(int requestedPosition) {
+ return isInfinite()
+ && (requestedPosition <= RESET_BOUND
+ || requestedPosition >= (Integer.MAX_VALUE - RESET_BOUND));
+ }
+
+ private void ensureValidPosition(int position) {
+ if (position >= wrapped.getItemCount()) {
+ throw new IndexOutOfBoundsException(String.format(Locale.US,
+ "requested position is outside adapter's bounds: position=%d, size=%d",
+ position, wrapped.getItemCount()));
+ }
+ }
+
+ private boolean isInfinite() {
+ return wrapped.getItemCount() > 1;
+ }
+
+ @Override
+ public int getInitialPosition() {
+ return isInfinite() ? CENTER : 0;
+ }
+
+ private void setPosition(int position) {
+ layoutManager.scrollToPosition(position);
+ }
+
+ //TODO: handle proper data set change notifications
+ private class DataSetChangeDelegate extends RecyclerView.AdapterDataObserver {
+
+ @Override
+ public void onChanged() {
+ setPosition(getInitialPosition());
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ onChanged();
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ onChanged();
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ onChanged();
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ notifyItemRangeChanged(0, getItemCount());
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ notifyItemRangeChanged(0, getItemCount(), payload);
+ }
+ }
+}
\ No newline at end of file
diff --git a/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..7aac486
--- /dev/null
+++ b/library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java
@@ -0,0 +1,108 @@
+package com.yarolegovich.discretescrollview;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Created by yarolegovich on 10/25/17.
+ */
+public class RecyclerViewProxy {
+
+ private RecyclerView.LayoutManager layoutManager;
+
+ public RecyclerViewProxy(@NonNull RecyclerView.LayoutManager layoutManager) {
+ this.layoutManager = layoutManager;
+ }
+
+ public void attachView(View view) {
+ layoutManager.attachView(view);
+ }
+
+ public void detachView(View view) {
+ layoutManager.detachView(view);
+ }
+
+ public void detachAndScrapView(View view, RecyclerView.Recycler recycler) {
+ layoutManager.detachAndScrapView(view, recycler);
+ }
+
+ public void detachAndScrapAttachedViews(RecyclerView.Recycler recycler) {
+ layoutManager.detachAndScrapAttachedViews(recycler);
+ }
+
+ public void recycleView(View view, RecyclerView.Recycler recycler) {
+ recycler.recycleView(view);
+ }
+
+ public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
+ layoutManager.removeAndRecycleAllViews(recycler);
+ }
+
+ public int getChildCount() {
+ return layoutManager.getChildCount();
+ }
+
+ public int getItemCount() {
+ return layoutManager.getItemCount();
+ }
+
+ public View getMeasuredChildForAdapterPosition(int position, RecyclerView.Recycler recycler) {
+ View view = recycler.getViewForPosition(position);
+ layoutManager.addView(view);
+ layoutManager.measureChildWithMargins(view, 0, 0);
+ return view;
+ }
+
+ public void layoutDecoratedWithMargins(View v, int left, int top, int right, int bottom) {
+ layoutManager.layoutDecoratedWithMargins(v, left, top, right, bottom);
+ }
+
+ public View getChildAt(int index) {
+ return layoutManager.getChildAt(index);
+ }
+
+ public int getPosition(View view) {
+ return layoutManager.getPosition(view);
+ }
+
+ public int getMeasuredWidthWithMargin(View child) {
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
+ return layoutManager.getDecoratedMeasuredWidth(child) + lp.leftMargin + lp.rightMargin;
+ }
+
+ public int getMeasuredHeightWithMargin(View child) {
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
+ return layoutManager.getDecoratedMeasuredHeight(child) + lp.topMargin + lp.bottomMargin;
+ }
+
+ public int getWidth() {
+ return layoutManager.getWidth();
+ }
+
+ public int getHeight() {
+ return layoutManager.getHeight();
+ }
+
+ public void offsetChildrenHorizontal(int amount) {
+ layoutManager.offsetChildrenHorizontal(amount);
+ }
+
+ public void offsetChildrenVertical(int amount) {
+ layoutManager.offsetChildrenVertical(amount);
+ }
+
+ public void requestLayout() {
+ layoutManager.requestLayout();
+ }
+
+ public void startSmoothScroll(RecyclerView.SmoothScroller smoothScroller) {
+ layoutManager.startSmoothScroll(smoothScroller);
+ }
+
+ public void removeAllViews() {
+ layoutManager.removeAllViews();
+ }
+}
diff --git a/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 8ec8d4f..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,26 +1,24 @@
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.
*/
-
-public class ScaleTransformer implements DiscreteScrollItemTransformer {
+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;
}
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..11ad976 100644
--- a/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java
+++ b/library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java
@@ -1,7 +1,9 @@
package com.yarolegovich.discretescrollview.util;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.RecyclerView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
import com.yarolegovich.discretescrollview.DiscreteScrollView;
@@ -12,7 +14,7 @@ public class ScrollListenerAdapter implements
private DiscreteScrollView.ScrollListener adaptee;
- public ScrollListenerAdapter(DiscreteScrollView.ScrollListener adaptee) {
+ public ScrollListenerAdapter(@NonNull DiscreteScrollView.ScrollListener adaptee) {
this.adaptee = adaptee;
}
@@ -27,7 +29,18 @@ 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
+ 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
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..e11ba43
--- /dev/null
+++ b/library/src/test/java/com/yarolegovich/discretescrollview/DiscreteScrollLayoutManagerTest.java
@@ -0,0 +1,563 @@
+package com.yarolegovich.discretescrollview;
+
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+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.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Matchers.any;
+import static org.junit.Assert.*;
+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.
+ */
+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 DSVOrientation.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 DSVOrientation 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(nullable(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(stubState);
+
+ 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)), nullable(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(nullable(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..971dbcd
--- /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 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
new file mode 100644
index 0000000..070382b
--- /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 DSVOrientation getOrientationToTest() {
+ return DSVOrientation.VERTICAL;
+ }
+
+}
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..eb72168
--- /dev/null
+++ b/library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java
@@ -0,0 +1,195 @@
+package com.yarolegovich.discretescrollview.stub;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+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 getMeasuredWidthWithMargin(View child) {
+ return childWidth;
+ }
+
+ @Override
+ public int getMeasuredHeightWithMargin(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;
+ }
+ }
+}
diff --git a/release-bintray.gradle b/release-bintray.gradle
new file mode 100644
index 0000000..ec2206a
--- /dev/null
+++ b/release-bintray.gradle
@@ -0,0 +1,60 @@
+apply plugin: 'maven-publish'
+apply plugin: 'com.jfrog.bintray'
+
+def upload = [
+ user : 'yarolegovich',
+ artifactId : 'discrete-scrollview',
+ userOrg : 'yarolegovich',
+ repository : 'maven',
+ 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.',
+ version : '1.5.1',
+ licences : ['Apache-2.0']
+]
+
+task androidSourcesJar(type: Jar) {
+ archiveClassifier.set('sources')
+ from android.sourceSets.main.java.srcDirs
+}
+
+version upload.version
+
+afterEvaluate {
+
+ publishing {
+ publications {
+ LibRelease(MavenPublication) {
+ from components.release
+
+ artifact androidSourcesJar
+
+ artifactId upload.artifactId
+ groupId upload.groupId
+ version upload.version
+ }
+ }
+ }
+
+ Properties localProps = new Properties()
+ localProps.load(project.rootProject.file('local.properties').newDataInputStream())
+
+ bintray {
+ user = upload.user
+ key = localProps.getProperty('bintray.api_key')
+ publications = ['LibRelease']
+ configurations = ['archives']
+ pkg {
+ name = upload.uploadName
+ repo = upload.repository
+ userOrg = upload.userOrg
+ licenses = upload.licences
+ publish = true
+ dryRun = false
+ version {
+ name = upload.version
+ desc = upload.description
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample/build.gradle b/sample/build.gradle
index 91d0758..f684808 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -1,13 +1,14 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 25
- buildToolsVersion "25.0.2"
+ compileSdkVersion rootProject.compileSdkVersion
+ buildToolsVersion rootProject.buildToolsVersion
+
defaultConfig {
applicationId "com.yarolegovich.discretescrollview.sample"
minSdkVersion 19
- targetSdkVersion 25
- versionCode 3
+ targetSdkVersion rootProject.targetSdkVersion
+ versionCode 4
versionName "1.0"
}
@@ -20,15 +21,10 @@ 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.github.bumptech.glide:glide:3.7.0'
-
- compile 'com.yarolegovich:mp:1.0.5'
+ implementation deps.designSupport
+ implementation deps.annotations
+ implementation deps.glide
+ implementation deps.materialPrefs
- 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 cee1be8..6e6f894 100644
--- a/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java
+++ b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java
@@ -3,14 +3,17 @@
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
-import android.support.design.widget.BottomSheetDialog;
-import android.support.v7.preference.PreferenceManager;
-import android.support.v7.widget.PopupMenu;
+import android.preference.PreferenceManager;
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;
import java.lang.ref.WeakReference;
@@ -43,25 +46,35 @@ 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();
- for (int i = 0; i < scrollView.getAdapter().getItemCount(); i++) {
+ final RecyclerView.Adapter> adapter = scrollView.getAdapter();
+ int itemCount = (adapter instanceof InfiniteScrollAdapter) ?
+ ((InfiniteScrollAdapter>) adapter).getRealItemCount() :
+ (adapter != null ? adapter.getItemCount() : 0);
+ 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;
}
@@ -73,6 +86,7 @@ public static int getTransitionTime() {
return defaultPrefs().getInt(instance.KEY_TRANSITION_TIME, 150);
}
+ @SuppressWarnings("deprecation")
private static SharedPreferences defaultPrefs() {
return PreferenceManager.getDefaultSharedPreferences(App.getInstance());
}
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 c74fce8..1256838 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,12 +2,13 @@
import android.animation.ArgbEvaluator;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-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;
@@ -33,10 +34,10 @@ protected void onCreate(Bundle savedInstanceState) {
Gallery gallery = Gallery.get();
List data = gallery.getData();
- DiscreteScrollView itemPicker = (DiscreteScrollView) findViewById(R.id.item_picker);
+ DiscreteScrollView itemPicker = 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);
@@ -58,16 +59,22 @@ 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
- 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/gallery/GalleryAdapter.java b/sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryAdapter.java
index a502361..01f17fa 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());
@@ -66,7 +69,7 @@ static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View itemView) {
super(itemView);
- image = (ImageView) itemView.findViewById(R.id.image);
+ image = itemView.findViewById(R.id.image);
overlay = itemView.findViewById(R.id.overlay);
}
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..1aa6392 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.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.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.Orientation;
+import com.yarolegovich.discretescrollview.InfiniteScrollAdapter;
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,22 +33,24 @@ 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) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_shop);
- currentItemName = (TextView) findViewById(R.id.item_name);
- currentItemPrice = (TextView) findViewById(R.id.item_price);
- rateItemButton = (ImageView) findViewById(R.id.item_btn_rate);
+ currentItemName = findViewById(R.id.item_name);
+ currentItemPrice = findViewById(R.id.item_price);
+ rateItemButton = findViewById(R.id.item_btn_rate);
shop = Shop.get();
data = shop.getData();
- itemPicker = (DiscreteScrollView) findViewById(R.id.item_picker);
- itemPicker.setOrientation(Orientation.HORIZONTAL);
- itemPicker.setOnItemChangedListener(this);
- itemPicker.setAdapter(new ShopAdapter(data));
+ itemPicker = findViewById(R.id.item_picker);
+ itemPicker.setOrientation(DSVOrientation.HORIZONTAL);
+ itemPicker.addOnItemChangedListener(this);
+ infiniteAdapter = InfiniteScrollAdapter.wrap(new ShopAdapter(data));
+ itemPicker.setAdapter(infiniteAdapter);
itemPicker.setItemTransitionTimeMillis(DiscreteScrollViewOptions.getTransitionTime());
itemPicker.setItemTransformer(new ScaleTransformer.Builder()
.setMinScale(0.8f)
@@ -69,7 +71,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;
@@ -105,8 +108,9 @@ private void changeRateButtonState(Item item) {
}
@Override
- public void onCurrentItemChanged(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
- onItemChanged(data.get(position));
+ public void onCurrentItemChanged(@Nullable ShopAdapter.ViewHolder viewHolder, int adapterPosition) {
+ int positionInDataSet = infiniteAdapter.getRealPosition(adapterPosition);
+ onItemChanged(data.get(positionInDataSet));
}
private void showUnsupportedSnackBar() {
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..0beee57 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,13 +45,13 @@ public int getItemCount() {
return data.size();
}
- class ViewHolder extends RecyclerView.ViewHolder {
+ static class ViewHolder extends RecyclerView.ViewHolder {
private ImageView image;
public ViewHolder(View itemView) {
super(itemView);
- image = (ImageView) itemView.findViewById(R.id.image);
+ image = itemView.findViewById(R.id.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..8f79665 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());
@@ -66,8 +72,8 @@ class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener
public ViewHolder(View itemView) {
super(itemView);
- imageView = (ImageView) itemView.findViewById(R.id.city_image);
- textView = (TextView) itemView.findViewById(R.id.city_name);
+ imageView = itemView.findViewById(R.id.city_image);
+ textView = itemView.findViewById(R.id.city_name);
itemView.findViewById(R.id.container).setOnClickListener(this);
}
@@ -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..15b5c46 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();
@@ -61,9 +55,9 @@ public ForecastView(Context context, AttributeSet attrs, int defStyleAttr, int d
setGravity(Gravity.CENTER_HORIZONTAL);
inflate(getContext(), R.layout.view_forecast, this);
- weatherDescription = (TextView) findViewById(R.id.weather_description);
- weatherImage = (ImageView) findViewById(R.id.weather_image);
- weatherTemperature = (TextView) findViewById(R.id.weather_temperature);
+ weatherDescription = findViewById(R.id.weather_description);
+ weatherImage = findViewById(R.id.weather_image);
+ weatherTemperature = findViewById(R.id.weather_temperature);
}
private void initGradient() {
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..e69c7fa 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;
@@ -32,13 +34,14 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_weather);
- forecastView = (ForecastView) findViewById(R.id.forecast_view);
+ forecastView = findViewById(R.id.forecast_view);
forecasts = WeatherStation.get().getForecasts();
- cityPicker = (DiscreteScrollView) findViewById(R.id.forecast_city_picker);
+ cityPicker = findViewById(R.id.forecast_city_picker);
+ cityPicker.setSlideOnFling(true);
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 +56,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
@@ -66,12 +72,14 @@ 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);
+ 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" />
diff --git a/sample/src/main/res/layout/dialog_transition_time.xml b/sample/src/main/res/layout/dialog_transition_time.xml
index 3a0570d..4b2023d 100644
--- a/sample/src/main/res/layout/dialog_transition_time.xml
+++ b/sample/src/main/res/layout/dialog_transition_time.xml
@@ -24,6 +24,7 @@
android:layout_height="wrap_content"
android:layout_gravity="end"
android:padding="12dp"
+ android:contentDescription="@string/cd_back"
android:src="@drawable/ic_close_black_24dp"
android:tint="@color/grayIconTint" />
\ No newline at end of file
diff --git a/sample/src/main/res/layout/item_city_card.xml b/sample/src/main/res/layout/item_city_card.xml
index 8d4ddd6..b3092eb 100644
--- a/sample/src/main/res/layout/item_city_card.xml
+++ b/sample/src/main/res/layout/item_city_card.xml
@@ -1,5 +1,5 @@
-
+ tools:src="@drawable/washington"
+ tools:ignore="ContentDescription" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/sample/src/main/res/layout/item_gallery.xml b/sample/src/main/res/layout/item_gallery.xml
index 51266ad..2ddde9e 100644
--- a/sample/src/main/res/layout/item_gallery.xml
+++ b/sample/src/main/res/layout/item_gallery.xml
@@ -1,5 +1,6 @@
-
+ android:scaleType="centerCrop"
+ tools:ignore="ContentDescription" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/sample/src/main/res/layout/item_shop_card.xml b/sample/src/main/res/layout/item_shop_card.xml
index 6346b5b..d7942b4 100644
--- a/sample/src/main/res/layout/item_shop_card.xml
+++ b/sample/src/main/res/layout/item_shop_card.xml
@@ -1,5 +1,6 @@
-
+ android:layout_height="match_parent"
+ tools:ignore="ContentDescription" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/sample/src/main/res/layout/toolbar.xml b/sample/src/main/res/layout/toolbar.xml
index 5f9ca3e..8f8c0e4 100644
--- a/sample/src/main/res/layout/toolbar.xml
+++ b/sample/src/main/res/layout/toolbar.xml
@@ -1,5 +1,5 @@
-
+ android:textSize="32sp"
+ tools:text="°C" />
pref_key_transition_time
Rate on GitHub
+
+ Back
+ Comment
+ Favourite