├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── cards_shop.gif ├── cards_weather.gif └── screenshot_weather.jpg ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── yarolegovich │ │ └── discretescrollview │ │ ├── DataSetModificationTest.java │ │ ├── DiscreteScrollViewTest.java │ │ ├── ScrollFunctionalityTest.java │ │ ├── context │ │ ├── TestActivity.java │ │ ├── TestAdapter.java │ │ └── TestData.java │ │ └── custom │ │ └── CustomAssertions.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── yarolegovich │ │ │ └── discretescrollview │ │ │ ├── DSVOrientation.java │ │ │ ├── DSVScrollConfig.java │ │ │ ├── Direction.java │ │ │ ├── DiscreteScrollLayoutManager.java │ │ │ ├── DiscreteScrollView.java │ │ │ ├── InfiniteScrollAdapter.java │ │ │ ├── RecyclerViewProxy.java │ │ │ ├── transform │ │ │ ├── DiscreteScrollItemTransformer.java │ │ │ ├── Pivot.java │ │ │ └── ScaleTransformer.java │ │ │ └── util │ │ │ └── ScrollListenerAdapter.java │ └── res │ │ └── values │ │ ├── attr.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── yarolegovich │ └── discretescrollview │ ├── DiscreteScrollLayoutManagerTest.java │ ├── HorizontalDiscreteScrollLayoutManagerTest.java │ ├── VerticalDiscreteScrollLayoutManagerTest.java │ └── stub │ └── StubRecyclerViewProxy.java ├── release-bintray.gradle ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── sample-release.apk └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── yarolegovich │ │ └── discretescrollview │ │ └── sample │ │ ├── App.java │ │ ├── DiscreteScrollViewOptions.java │ │ ├── MainActivity.java │ │ ├── gallery │ │ ├── Gallery.java │ │ ├── GalleryActivity.java │ │ ├── GalleryAdapter.java │ │ └── Image.java │ │ ├── shop │ │ ├── Item.java │ │ ├── Shop.java │ │ ├── ShopActivity.java │ │ └── ShopAdapter.java │ │ └── weather │ │ ├── Forecast.java │ │ ├── ForecastAdapter.java │ │ ├── ForecastView.java │ │ ├── Weather.java │ │ ├── WeatherActivity.java │ │ └── WeatherStation.java │ └── res │ ├── drawable-hdpi │ ├── ic_arrow_back_black_24dp.png │ ├── ic_behance_black_24dp.png │ ├── ic_close_black_24dp.png │ ├── ic_collections_black_24dp.png │ ├── ic_comment_text_outline_black_24dp.png │ ├── ic_github_circle_white_24dp.png │ ├── ic_share_white_24dp.png │ ├── ic_shopping_black_24dp.png │ ├── ic_star_black_24dp.png │ ├── ic_star_border_black_24dp.png │ └── ic_weather_partlycloudy_black_24dp.png │ ├── drawable-mdpi │ ├── ic_arrow_back_black_24dp.png │ ├── ic_behance_black_24dp.png │ ├── ic_close_black_24dp.png │ ├── ic_collections_black_24dp.png │ ├── ic_comment_text_outline_black_24dp.png │ ├── ic_github_circle_white_24dp.png │ ├── ic_share_white_24dp.png │ ├── ic_shopping_black_24dp.png │ ├── ic_star_black_24dp.png │ ├── ic_star_border_black_24dp.png │ └── ic_weather_partlycloudy_black_24dp.png │ ├── drawable-nodpi │ ├── london.png │ ├── new_york.png │ ├── paris.png │ ├── pisa.png │ ├── rome.png │ └── washington.png │ ├── drawable-xhdpi │ ├── ic_arrow_back_black_24dp.png │ ├── ic_behance_black_24dp.png │ ├── ic_close_black_24dp.png │ ├── ic_collections_black_24dp.png │ ├── ic_comment_text_outline_black_24dp.png │ ├── ic_github_circle_white_24dp.png │ ├── ic_share_white_24dp.png │ ├── ic_shopping_black_24dp.png │ ├── ic_star_black_24dp.png │ ├── ic_star_border_black_24dp.png │ └── ic_weather_partlycloudy_black_24dp.png │ ├── drawable-xxhdpi │ ├── ic_arrow_back_black_24dp.png │ ├── ic_behance_black_24dp.png │ ├── ic_close_black_24dp.png │ ├── ic_collections_black_24dp.png │ ├── ic_comment_text_outline_black_24dp.png │ ├── ic_github_circle_white_24dp.png │ ├── ic_share_white_24dp.png │ ├── ic_shopping_black_24dp.png │ ├── ic_star_black_24dp.png │ ├── ic_star_border_black_24dp.png │ └── ic_weather_partlycloudy_black_24dp.png │ ├── drawable-xxxhdpi │ ├── ic_arrow_back_black_24dp.png │ ├── ic_behance_black_24dp.png │ ├── ic_close_black_24dp.png │ ├── ic_collections_black_24dp.png │ ├── ic_comment_text_outline_black_24dp.png │ ├── ic_github_circle_white_24dp.png │ ├── ic_share_white_24dp.png │ ├── ic_shopping_black_24dp.png │ ├── ic_star_black_24dp.png │ ├── ic_star_border_black_24dp.png │ └── ic_weather_partlycloudy_black_24dp.png │ ├── drawable │ ├── clear.png │ ├── cloudy.png │ ├── mostly_cloudy.png │ ├── partly_cloudy.png │ ├── periodic_clouds.png │ ├── shop1.jpg │ ├── shop2.jpg │ ├── shop3.jpg │ ├── shop4.jpg │ ├── shop5.jpg │ └── shop6.jpg │ ├── layout │ ├── activity_gallery.xml │ ├── activity_main.xml │ ├── activity_shop.xml │ ├── activity_weather.xml │ ├── dialog_transition_time.xml │ ├── item_city_card.xml │ ├── item_gallery.xml │ ├── item_shop_card.xml │ ├── toolbar.xml │ └── view_forecast.xml │ ├── menu │ └── main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea* 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DiscreteScrollView 2 | 3 | The library is a RecyclerView-based implementation of a scrollable list, where current item is centered and can be changed using swipes. 4 | It is similar to a ViewPager, but you can quickly and painlessly create layout, where views adjacent to the currently selected view are partially or fully visible on the screen. 5 | 6 | ![GifSampleShop](https://github.com/yarolegovich/DiscreteScrollView/blob/master/images/cards_shop.gif) 7 | 8 | ## Gradle 9 | Add this into your dependencies block. 10 | ``` 11 | compile 'com.yarolegovich:discrete-scrollview:1.5.1' 12 | ``` 13 | 14 | ## Reporting an issue 15 | 16 | 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. 17 | 18 | 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. 19 | 20 | ## Sample 21 | Get it on Google Play
22 | 23 | Please see the [sample app](https://github.com/yarolegovich/DiscreteScrollView/tree/master/sample/src/main/java/com/yarolegovich/discretescrollview/sample) for examples of library usage. 24 | 25 | ![GifSampleWeather](https://github.com/yarolegovich/DiscreteScrollView/blob/master/images/cards_weather.gif) 26 | 27 | ## Wiki 28 | ### General 29 | The library uses a custom LayoutManager to adjust items' positions on the screen and handle scroll, however it is not exposed to the client 30 | code. All public API is accessible through DiscreteScrollView class, which is a simple descendant of RecyclerView. 31 | 32 | If you have ever used RecyclerView - you already know how to use this library. One thing to note - you should NOT set LayoutManager. 33 | 34 | #### Usage: 35 | 1. Add DiscreteScrollView to your layout either using xml or code: 36 | 2. Create your implementation of RecyclerView.Adapter. Refer to the [sample](https://github.com/yarolegovich/DiscreteScrollView/blob/master/sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopAdapter.java) for an example, if you don't know how to do it. 37 | 3. Set the adapter. 38 | 4. You are done! 39 | ```xml 40 | 45 | ``` 46 | ```java 47 | DiscreteScrollView scrollView = findViewById(R.id.picker); 48 | scrollView.setAdapter(new YourAdapterImplementation()); 49 | ``` 50 | 51 | ### API 52 | #### General 53 | ```java 54 | scrollView.setOrientation(DSVOrientation o); //Sets an orientation of the view 55 | scrollView.setOffscreenItems(count); //Reserve extra space equal to (childSize * count) on each side of the view 56 | scrollView.setOverScrollEnabled(enabled); //Can also be set using android:overScrollMode xml attribute 57 | ``` 58 | #### Related to the current item: 59 | ```java 60 | scrollView.getCurrentItem(); //returns adapter position of the currently selected item or -1 if adapter is empty. 61 | scrollView.scrollToPosition(int position); //position becomes selected 62 | scrollView.smoothScrollToPosition(int position); //position becomes selected with animated scroll 63 | scrollView.setItemTransitionTimeMillis(int millis); //determines how much time it takes to change the item on fling, settle or smoothScroll 64 | ``` 65 | #### Transformations 66 | One useful feature of ViewPager is page transformations. It allows you, for example, to create carousel effect. DiscreteScrollView also supports 67 | page transformations. 68 | ```java 69 | scrollView.setItemTransformer(transformer); 70 | 71 | public interface DiscreteScrollItemTransformer { 72 | /** 73 | * In this method you apply any transform you can imagine (perfomance is not guaranteed). 74 | * @param position is a value inside the interval [-1f..1f]. In idle state: 75 | * |view1| |currentlySelectedView| |view2| 76 | * -view1 and everything to the left is on position -1; 77 | * -currentlySelectedView is on position 0; 78 | * -view2 and everything to the right is on position 1. 79 | */ 80 | void transformItem(View item, float position); 81 | } 82 | ``` 83 | In the above example `view1Position == (currentlySelectedViewPosition - n)` and `view2Position == (currentlySelectedViewPosition + n)`, where `n` defaults to 1 and can be changed using the following API: 84 | ```java 85 | scrollView.setClampTransformProgressAfter(n); 86 | ``` 87 | Because scale transformation is the most common, I included a helper class - ScaleTransformer, here is how to use it: 88 | ```java 89 | cityPicker.setItemTransformer(new ScaleTransformer.Builder() 90 | .setMaxScale(1.05f) 91 | .setMinScale(0.8f) 92 | .setPivotX(Pivot.X.CENTER) // CENTER is a default one 93 | .setPivotY(Pivot.Y.BOTTOM) // CENTER is a default one 94 | .build()); 95 | ``` 96 | You may see how it works on GIFs. 97 | 98 | #### Slide through multiple items 99 | 100 | To allow slide through multiple items call: 101 | ```java 102 | scrollView.setSlideOnFling(true); 103 | ``` 104 | The default threshold is set to 2100. Lower the threshold, more fluid the animation. You can adjust the threshold by calling: 105 | ```java 106 | scrollView.setSlideOnFlingThreshold(value); 107 | ``` 108 | 109 | #### Infinite scroll 110 | Infinite scroll is implemented on the adapter level: 111 | ```java 112 | InfiniteScrollAdapter wrapper = InfiniteScrollAdapter.wrap(yourAdapter); 113 | scrollView.setAdapter(wrapper); 114 | ``` 115 | An instance of `InfiniteScrollAdapter` has the following useful methods: 116 | ```java 117 | int getRealItemCount(); 118 | 119 | int getRealCurrentPosition(); 120 | 121 | int getRealPosition(int position); 122 | 123 | /* 124 | * You will probably want this method in the following use case: 125 | * int targetAdapterPosition = wrapper.getClosestPosition(targetPosition); 126 | * scrollView.smoothScrollTo(targetAdapterPosition); 127 | * To scroll the data set for the least required amount to reach targetPosition. 128 | */ 129 | int getClosestPosition(int position); 130 | ``` 131 | Currently `InfiniteScrollAdapter` handles data set changes inefficiently, so your contributions are welcome. 132 | #### Disabling scroll 133 | It's possible to forbid user scroll in any or specific direction using: 134 | ```java 135 | scrollView.setScrollConfig(config); 136 | ``` 137 | Where `config` is an instance of `DSVScrollConfig` enum. The default value enables scroll in any direction. 138 | #### Callbacks 139 | * Scroll state changes: 140 | ```java 141 | scrollView.addScrollStateChangeListener(listener); 142 | scrollView.removeScrollStateChangeListener(listener); 143 | 144 | public interface ScrollStateChangeListener { 145 | 146 | void onScrollStart(T currentItemHolder, int adapterPosition); //called when scroll is started, including programatically initiated scroll 147 | 148 | void onScrollEnd(T currentItemHolder, int adapterPosition); //called when scroll ends 149 | /** 150 | * Called when scroll is in progress. 151 | * @param scrollPosition is a value inside the interval [-1f..1f], it corresponds to the position of currentlySelectedView. 152 | * In idle state: 153 | * |view1| |currentlySelectedView| |view2| 154 | * -view1 is on position -1; 155 | * -currentlySelectedView is on position 0; 156 | * -view2 is on position 1. 157 | * @param currentIndex - index of current view 158 | * @param newIndex - index of a view which is becoming the new current 159 | * @param currentHolder - ViewHolder of a current view 160 | * @param newCurrent - ViewHolder of a view which is becoming the new current 161 | */ 162 | void onScroll(float scrollPosition, int currentIndex, int newIndex, @Nullable T currentHolder, @Nullable T newCurrentHolder); 163 | } 164 | ``` 165 | * Scroll: 166 | ```java 167 | scrollView.addScrollListener(listener); 168 | scrollView.removeScrollListener(listener); 169 | 170 | public interface ScrollListener { 171 | //The same as ScrollStateChangeListener, but for the cases when you are interested only in onScroll() 172 | void onScroll(float scrollPosition, int currentIndex, int newIndex, @Nullable T currentHolder, @Nullable T newCurrentHolder); 173 | } 174 | ``` 175 | * Current selection changes: 176 | ```java 177 | scrollView.addOnItemChangedListener(listener); 178 | scrollView.removeOnItemChangedListener(listener); 179 | 180 | public interface OnItemChangedListener { 181 | /** 182 | * Called when new item is selected. It is similar to the onScrollEnd of ScrollStateChangeListener, except that it is 183 | * also called when currently selected item appears on the screen for the first time. 184 | * viewHolder will be null, if data set becomes empty 185 | */ 186 | void onCurrentItemChanged(@Nullable T viewHolder, int adapterPosition); 187 | } 188 | ``` 189 | 190 | ## Special thanks 191 | Thanks to [Tayisiya Yurkiv](https://www.behance.net/yurkivt) for sample app design and beautiful GIFs. 192 | 193 | ## License 194 | ``` 195 | Copyright 2017 Yaroslav Shevchuk 196 | 197 | Licensed under the Apache License, Version 2.0 (the "License"); 198 | you may not use this file except in compliance with the License. 199 | You may obtain a copy of the License at 200 | 201 | http://www.apache.org/licenses/LICENSE-2.0 202 | 203 | Unless required by applicable law or agreed to in writing, software 204 | distributed under the License is distributed on an "AS IS" BASIS, 205 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 206 | See the License for the specific language governing permissions and 207 | limitations under the License. 208 | ``` 209 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | google() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:4.0.1' 8 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | jcenter() 15 | maven { url "https://maven.google.com" } 16 | maven { url "https://jitpack.io" } 17 | google() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | 25 | ext { 26 | compileSdkVersion = 29 27 | buildToolsVersion = '29.0.2' 28 | targetSdkVersion = 29 29 | 30 | deps = [ 31 | recycler : 'androidx.recyclerview:recyclerview:1.0.0', 32 | designSupport : 'com.google.android.material:material:1.0.0', 33 | annotations : 'androidx.annotation:annotation:1.1.0', 34 | androidxCompat: 'androidx.appcompat:appcompat:1.1.0', 35 | glide : 'com.github.bumptech.glide:glide:4.11.0', 36 | materialPrefs : 'com.yarolegovich:mp:1.1.6' 37 | ] 38 | 39 | testDeps = [ 40 | hamcrest : 'org.hamcrest:hamcrest-library:1.3', 41 | mockito : 'org.mockito:mockito-core:2.13.0', 42 | jUnit : 'junit:junit:4.13', 43 | robolectric : 'org.robolectric:robolectric:3.0', 44 | espresso : 'androidx.test.espresso:espresso-core:3.1.0', 45 | androidJUnit: 'androidx.test.ext:junit:1.1.1', 46 | testRules : 'androidx.test:rules:1.1.1', 47 | testRunner : 'androidx.test:runner:1.1.1' 48 | ] 49 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | 3 | org.gradle.jvmargs=-Xmx1536m -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jul 30 09:08:49 EEST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/cards_shop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/images/cards_shop.gif -------------------------------------------------------------------------------- /images/cards_weather.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/images/cards_weather.gif -------------------------------------------------------------------------------- /images/screenshot_weather.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/images/screenshot_weather.jpg -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply from: rootProject.file('release-bintray.gradle') 3 | 4 | android { 5 | compileSdkVersion rootProject.compileSdkVersion 6 | buildToolsVersion rootProject.buildToolsVersion 7 | 8 | defaultConfig { 9 | minSdkVersion 14 10 | targetSdkVersion rootProject.targetSdkVersion 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | } 17 | 18 | dependencies { 19 | implementation deps.recycler 20 | implementation deps.annotations 21 | 22 | testImplementation testDeps.robolectric 23 | testImplementation testDeps.jUnit 24 | testImplementation testDeps.mockito 25 | testImplementation testDeps.hamcrest 26 | 27 | debugImplementation deps.androidxCompat 28 | androidTestImplementation testDeps.espresso 29 | androidTestImplementation testDeps.androidJUnit 30 | androidTestImplementation testDeps.testRunner 31 | androidTestImplementation testDeps.testRules 32 | androidTestImplementation testDeps.hamcrest 33 | } -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\yarolegovich\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /library/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/yarolegovich/discretescrollview/DataSetModificationTest.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | 4 | import androidx.test.ext.junit.runners.AndroidJUnit4; 5 | 6 | import com.yarolegovich.discretescrollview.context.TestData; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static androidx.test.espresso.matcher.ViewMatchers.assertThat; 15 | import static com.yarolegovich.discretescrollview.custom.CustomAssertions.currentPositionIs; 16 | import static com.yarolegovich.discretescrollview.custom.CustomAssertions.doesNotHaveChildren; 17 | import static org.hamcrest.Matchers.equalTo; 18 | import static org.hamcrest.Matchers.greaterThan; 19 | import static org.hamcrest.Matchers.is; 20 | 21 | /** 22 | * Created by yarolegovich on 2/3/18. 23 | */ 24 | @RunWith(AndroidJUnit4.class) 25 | public class DataSetModificationTest extends DiscreteScrollViewTest { 26 | 27 | @Test 28 | public void notifyItemInserted_afterCurrentPosition_currentPositionIsNotAffected() { 29 | final int initialPosition = scrollView.getCurrentItem(); 30 | onUiThread(new Runnable() { 31 | @Override 32 | public void run() { 33 | List data = adapter.getData(); 34 | data.add(initialPosition + 1, new TestData()); 35 | adapter.notifyItemInserted(initialPosition + 1); 36 | } 37 | }); 38 | onScrollView().check(currentPositionIs(initialPosition)); 39 | } 40 | 41 | @Test 42 | public void notifyItemInserted_beforeCurrentPosition_currentPositionIsShifterRightByOne() { 43 | final int initialPosition = scrollView.getCurrentItem(); 44 | onUiThread(new Runnable() { 45 | @Override 46 | public void run() { 47 | List data = adapter.getData(); 48 | data.add(initialPosition, new TestData()); 49 | adapter.notifyItemInserted(initialPosition); 50 | } 51 | }); 52 | onScrollView().check(currentPositionIs(initialPosition + 1)); 53 | } 54 | 55 | @Test 56 | public void notifyItemRemoved_afterCurrentPosition_currentPositionIsNotAffected() { 57 | final int initialPosition = scrollView.getCurrentItem(); 58 | assertThat(adapter.getItemCount(), is(greaterThan(1))); 59 | onUiThread(new Runnable() { 60 | @Override 61 | public void run() { 62 | List data = adapter.getData(); 63 | data.remove(initialPosition + 1); 64 | adapter.notifyItemRemoved(initialPosition + 1); 65 | } 66 | }); 67 | onScrollView().check(currentPositionIs(initialPosition)); 68 | } 69 | 70 | @Test 71 | public void notifyItemRemoved_beforeCurrentPosition_currentPositionIsShifterLeftByOne() { 72 | assertThat(adapter.getItemCount(), is(greaterThan(1))); 73 | final int initialPosition = adapter.getItemCount() / 2; 74 | ensurePositionIs(initialPosition); 75 | onUiThread(new Runnable() { 76 | @Override 77 | public void run() { 78 | List data = adapter.getData(); 79 | data.remove(initialPosition - 1); 80 | adapter.notifyItemRemoved(initialPosition - 1); 81 | } 82 | }); 83 | onScrollView().check(currentPositionIs(initialPosition - 1)); 84 | } 85 | 86 | @Test 87 | public void notifyItemInserted_multipleInsertsBeforeCurrent_currentIsShiftedCorrectly() { 88 | final int numberOfInserts = 5; 89 | final int initialPosition = scrollView.getCurrentItem(); 90 | onUiThread(new Runnable() { 91 | @Override 92 | public void run() { 93 | List data = adapter.getData(); 94 | for (int i = 0; i < numberOfInserts; i++) { 95 | data.add(initialPosition, new TestData()); 96 | adapter.notifyItemInserted(initialPosition); 97 | } 98 | } 99 | }); 100 | onScrollView().check(currentPositionIs(initialPosition + numberOfInserts)); 101 | } 102 | 103 | @Test 104 | public void notifyItemRemoved_calledUntilEmpty_scrollViewIsEmpty() { 105 | onUiThread(new Runnable() { 106 | @Override 107 | public void run() { 108 | List data = adapter.getData(); 109 | while (data.size() > 0) { 110 | data.remove(0); 111 | adapter.notifyItemRemoved(0); 112 | } 113 | } 114 | }); 115 | onScrollView().check(doesNotHaveChildren()); 116 | } 117 | 118 | @Test 119 | public void notifyItemRangeInserted_afterCurrentPosition_positionIsNotAffected() { 120 | final int numOfItemsToInsert = 5; 121 | final int initialPosition = scrollView.getCurrentItem(); 122 | onUiThread(new Runnable() { 123 | @Override 124 | public void run() { 125 | List data = adapter.getData(); 126 | data.addAll(initialPosition + 1, createItems(numOfItemsToInsert)); 127 | adapter.notifyItemRangeInserted(initialPosition + 1, numOfItemsToInsert); 128 | } 129 | }); 130 | onScrollView().check(currentPositionIs(initialPosition)); 131 | } 132 | 133 | @Test 134 | public void notifyItemRangeInserted_beforeCurrentPosition_positionIsShiftedRightByRangeLength() { 135 | final int numOfItemsToInsert = 5; 136 | final int initialPosition = scrollView.getCurrentItem(); 137 | onUiThread(new Runnable() { 138 | @Override 139 | public void run() { 140 | List data = adapter.getData(); 141 | data.addAll(initialPosition, createItems(numOfItemsToInsert)); 142 | adapter.notifyItemRangeInserted(initialPosition, numOfItemsToInsert); 143 | } 144 | }); 145 | onScrollView().check(currentPositionIs(initialPosition + numOfItemsToInsert)); 146 | } 147 | 148 | @Test 149 | public void notifyItemRangeRemoved_afterCurrentPosition_positionIsNotAffected() { 150 | final int initialPosition = scrollView.getCurrentItem(); 151 | final int initialSize = adapter.getItemCount(); 152 | onUiThread(new Runnable() { 153 | @Override 154 | public void run() { 155 | List data = adapter.getData(); 156 | List toRemove = new ArrayList<>(); 157 | for (int i = initialPosition + 1; i < adapter.getItemCount() - 1; i++) { 158 | toRemove.add(data.get(i)); 159 | } 160 | assertThat(toRemove.size(), is(greaterThan(1))); 161 | data.removeAll(toRemove); 162 | assertThat(data.size(), is(equalTo(initialSize - toRemove.size()))); 163 | adapter.notifyItemRangeRemoved(initialPosition + 1, toRemove.size()); 164 | } 165 | }); 166 | onScrollView().check(currentPositionIs(initialPosition)); 167 | } 168 | 169 | @Test 170 | public void notifyItemRangeRemoved_beforeCurrentPosition_positionIsShiftedLeftByRangeLength() { 171 | assertThat(adapter.getItemCount(), is(greaterThan(2))); 172 | final int initialPosition = adapter.getItemCount() - 1; 173 | final int numOfItemsToRemove = adapter.getItemCount() - 1; 174 | ensurePositionIs(initialPosition); 175 | onUiThread(new Runnable() { 176 | @Override 177 | public void run() { 178 | final int initialSize = adapter.getItemCount(); 179 | List data = adapter.getData(); 180 | List toRemove = new ArrayList<>(); 181 | for (int i = initialPosition - 1; i >= 0; i--) { 182 | toRemove.add(data.get(i)); 183 | } 184 | assertThat(toRemove.size(), is(equalTo(numOfItemsToRemove))); 185 | data.removeAll(toRemove); 186 | assertThat(data.size(), is(equalTo(initialSize - toRemove.size()))); 187 | adapter.notifyItemRangeRemoved(0, toRemove.size()); 188 | } 189 | }); 190 | onScrollView().check(currentPositionIs(initialPosition - numOfItemsToRemove)); 191 | } 192 | 193 | @Test 194 | public void notifyDataSetChanged_currentItemRemainsInItemRange_currentIsNotAffected() { 195 | final int initialPosition = 0; 196 | ensurePositionIs(initialPosition); 197 | onUiThread(new Runnable() { 198 | @Override 199 | public void run() { 200 | List data = adapter.getData(); 201 | assertThat(data.size(), is(greaterThan(2))); 202 | data.remove(data.size() - 1); 203 | data.remove(data.size() - 1); 204 | adapter.notifyDataSetChanged(); 205 | } 206 | }); 207 | onScrollView().check(currentPositionIs(initialPosition)); 208 | } 209 | 210 | @Test 211 | public void notifyDataSetChanged_currentItemGoesOutsideItemRange_currentIsClampedToRange() { 212 | final int initialPosition = adapter.getItemCount() - 1; 213 | final int numOfItemsToRemove = 2; 214 | assertThat(adapter.getItemCount(), is(greaterThan(numOfItemsToRemove))); 215 | ensurePositionIs(initialPosition); 216 | onUiThread(new Runnable() { 217 | @Override 218 | public void run() { 219 | List data = adapter.getData(); 220 | data.subList(0, numOfItemsToRemove).clear(); 221 | adapter.notifyDataSetChanged(); 222 | } 223 | }); 224 | onScrollView().check(currentPositionIs(adapter.getItemCount() - 1)); 225 | } 226 | 227 | @Test 228 | public void notifyDataSetChanged_allItemsRemoved_scrollViewIsEmpty() { 229 | onUiThread(new Runnable() { 230 | @Override 231 | public void run() { 232 | adapter.getData().clear(); 233 | adapter.notifyDataSetChanged(); 234 | } 235 | }); 236 | onScrollView().check(doesNotHaveChildren()); 237 | } 238 | 239 | @Test 240 | public void notifyDataSetChanged_scrollToPositionCalledAfterItemsAdded_positionIsCorrect() { 241 | final int targetPosition = adapter.getItemCount(); 242 | onUiThread(new Runnable() { 243 | @Override 244 | public void run() { 245 | List data = adapter.getData(); 246 | final int itemsToAdd = data.size(); 247 | for (int i = 0; i < itemsToAdd; i++) { 248 | data.add(new TestData()); 249 | } 250 | adapter.notifyDataSetChanged(); 251 | scrollView.scrollToPosition(targetPosition); 252 | } 253 | }); 254 | onScrollView().check(currentPositionIs(targetPosition)); 255 | } 256 | 257 | @Test 258 | public void notifyDataSetChanged_scrollToPositionCalledAfterItemsRemoved_positionIsCorrect() { 259 | final int initialPosition = adapter.getItemCount() - 1; 260 | final int targetPosition = adapter.getItemCount() / 4; 261 | assertThat(targetPosition, is(greaterThan(0))); 262 | ensurePositionIs(initialPosition); 263 | onUiThread(new Runnable() { 264 | @Override 265 | public void run() { 266 | List data = adapter.getData(); 267 | final int itemsToRemove = data.size() / 2; 268 | if (itemsToRemove > 0) { 269 | data.subList(0, itemsToRemove).clear(); 270 | } 271 | adapter.notifyDataSetChanged(); 272 | scrollView.scrollToPosition(targetPosition); 273 | } 274 | }); 275 | onScrollView().check(currentPositionIs(targetPosition)); 276 | } 277 | 278 | private List createItems(int count) { 279 | List result = new ArrayList<>(count); 280 | for (int i = 0; i < count; i++) { 281 | result.add(new TestData()); 282 | } 283 | return result; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/yarolegovich/discretescrollview/DiscreteScrollViewTest.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.CallSuper; 6 | import androidx.test.espresso.Espresso; 7 | import androidx.test.espresso.IdlingRegistry; 8 | import androidx.test.espresso.IdlingResource; 9 | import androidx.test.espresso.ViewInteraction; 10 | import androidx.test.rule.ActivityTestRule; 11 | 12 | import com.yarolegovich.discretescrollview.context.TestActivity; 13 | import com.yarolegovich.discretescrollview.context.TestAdapter; 14 | 15 | import org.hamcrest.Matchers; 16 | import org.junit.After; 17 | import org.junit.Before; 18 | import org.junit.Rule; 19 | 20 | import java.util.List; 21 | 22 | import static com.yarolegovich.discretescrollview.custom.CustomAssertions.currentPositionIs; 23 | 24 | /** 25 | * Created by yarolegovich on 2/3/18. 26 | */ 27 | 28 | public abstract class DiscreteScrollViewTest { 29 | 30 | private IdlingResource[] idlingResources; 31 | 32 | protected DiscreteScrollView scrollView; 33 | protected TestAdapter adapter; 34 | 35 | @Rule 36 | public ActivityTestRule testActivity = new ActivityTestRule<>(TestActivity.class); 37 | 38 | @Before 39 | @CallSuper 40 | public void setUp() { 41 | TestActivity activity = testActivity.getActivity(); 42 | scrollView = activity.getScrollView(); 43 | adapter = testActivity.getActivity().getAdapter(); 44 | 45 | List resources = activity.getIdlingResources(); 46 | idlingResources = resources.toArray(new IdlingResource[resources.size()]); 47 | IdlingRegistry.getInstance().register(idlingResources); 48 | } 49 | 50 | @After 51 | @CallSuper 52 | public void tearDown() { 53 | IdlingRegistry.getInstance().unregister(idlingResources); 54 | } 55 | 56 | protected ViewInteraction onScrollView() { 57 | return Espresso.onView(Matchers.is(scrollView)); 58 | } 59 | 60 | protected void waitUntilScrollEnd() { 61 | testActivity.getActivity().incrementExpectedScrollEndCalls(); 62 | } 63 | 64 | protected void ensurePositionIs(final int position) { 65 | onUiThread(new Runnable() { 66 | @Override 67 | public void run() { 68 | scrollView.scrollToPosition(position); 69 | } 70 | }); 71 | onScrollView().check(currentPositionIs(position)); 72 | } 73 | 74 | protected void onUiThread(Runnable runnable) { 75 | try { 76 | testActivity.runOnUiThread(runnable); 77 | } catch (Throwable throwable) { 78 | throw new RuntimeException(throwable); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/yarolegovich/discretescrollview/ScrollFunctionalityTest.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | import static androidx.test.espresso.matcher.ViewMatchers.assertThat; 9 | import static com.yarolegovich.discretescrollview.custom.CustomAssertions.currentPositionIs; 10 | import static org.hamcrest.Matchers.greaterThan; 11 | import static org.hamcrest.Matchers.is; 12 | import static org.hamcrest.Matchers.lessThan; 13 | 14 | /** 15 | * Created by yarolegovich on 2/5/18. 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ScrollFunctionalityTest extends DiscreteScrollViewTest { 19 | 20 | @Test 21 | public void scrollToPosition_afterCurrent_changesPosition() { 22 | final int initialPosition = scrollView.getCurrentItem(); 23 | assertThat(initialPosition, is(lessThan(adapter.getItemCount() - 1))); 24 | onUiThread(new Runnable() { 25 | @Override 26 | public void run() { 27 | scrollView.scrollToPosition(initialPosition + 1); 28 | } 29 | }); 30 | onScrollView().check(currentPositionIs(initialPosition + 1)); 31 | } 32 | 33 | @Test 34 | public void scrollToPosition_beforeCurrent_changesPosition() { 35 | assertThat(adapter.getItemCount(), is(greaterThan(1))); 36 | final int initialPosition = adapter.getItemCount() / 2; 37 | ensurePositionIs(initialPosition); 38 | onUiThread(new Runnable() { 39 | @Override 40 | public void run() { 41 | scrollView.scrollToPosition(initialPosition - 1); 42 | } 43 | }); 44 | onScrollView().check(currentPositionIs(initialPosition - 1)); 45 | } 46 | 47 | @Test 48 | public void smoothScrollToPosition_afterCurrent_changesPosition() { 49 | final int initialPosition = scrollView.getCurrentItem(); 50 | assertThat(initialPosition, is(lessThan(adapter.getItemCount() - 1))); 51 | waitUntilScrollEnd(); 52 | onUiThread(new Runnable() { 53 | @Override 54 | public void run() { 55 | scrollView.setItemTransitionTimeMillis(10); 56 | scrollView.smoothScrollToPosition(initialPosition + 1); 57 | } 58 | }); 59 | onScrollView().check(currentPositionIs(initialPosition + 1)); 60 | } 61 | 62 | @Test 63 | public void smoothScrollToPosition_beforeCurrent_changesPosition() { 64 | assertThat(adapter.getItemCount(), is(greaterThan(1))); 65 | final int initialPosition = adapter.getItemCount() / 2; 66 | ensurePositionIs(initialPosition); 67 | waitUntilScrollEnd(); 68 | onUiThread(new Runnable() { 69 | @Override 70 | public void run() { 71 | scrollView.setItemTransitionTimeMillis(10); 72 | scrollView.smoothScrollToPosition(initialPosition - 1); 73 | } 74 | }); 75 | onScrollView().check(currentPositionIs(initialPosition - 1)); 76 | } 77 | 78 | @Test 79 | public void smoothScrollToPosition_throughSeveralPositionsAfterCurrent_changesPosition() { 80 | final int initialPosition = scrollView.getCurrentItem(); 81 | final int targetPosition = adapter.getItemCount() - 1; 82 | assertThat(targetPosition - initialPosition, is(greaterThan(1))); 83 | waitUntilScrollEnd(); 84 | onUiThread(new Runnable() { 85 | @Override 86 | public void run() { 87 | scrollView.setItemTransitionTimeMillis(10); 88 | scrollView.smoothScrollToPosition(targetPosition); 89 | } 90 | }); 91 | onScrollView().check(currentPositionIs(targetPosition)); 92 | } 93 | 94 | @Test 95 | public void smoothScrollToPosition_throughSeveralPositionsBeforeCurrent_changesPosition() { 96 | final int initialPosition = adapter.getItemCount() - 1; 97 | final int targetPosition = 0; 98 | ensurePositionIs(initialPosition); 99 | assertThat(initialPosition, is(greaterThan(0))); 100 | waitUntilScrollEnd(); 101 | onUiThread(new Runnable() { 102 | @Override 103 | public void run() { 104 | scrollView.setItemTransitionTimeMillis(10); 105 | scrollView.smoothScrollToPosition(targetPosition); 106 | } 107 | }); 108 | onScrollView().check(currentPositionIs(targetPosition)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestActivity.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.context; 2 | 3 | import android.os.Bundle; 4 | import android.view.Gravity; 5 | import android.view.ViewGroup; 6 | import android.widget.FrameLayout; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | import androidx.appcompat.app.AppCompatActivity; 11 | import androidx.recyclerview.widget.RecyclerView; 12 | import androidx.test.espresso.IdlingResource; 13 | import androidx.test.espresso.idling.CountingIdlingResource; 14 | 15 | import com.yarolegovich.discretescrollview.DiscreteScrollView; 16 | import com.yarolegovich.discretescrollview.R; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Collections; 20 | import java.util.List; 21 | 22 | import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 23 | import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 24 | 25 | /** 26 | * Created by yarolegovich on 2/4/18. 27 | */ 28 | 29 | public class TestActivity extends AppCompatActivity implements DiscreteScrollView.ScrollStateChangeListener { 30 | 31 | private DiscreteScrollView scrollView; 32 | private TestAdapter adapter; 33 | 34 | private CountingIdlingResource expectedScrollEndCalls = new CountingIdlingResource( 35 | "scrollEndCalls" + hashCode(), 36 | true); 37 | 38 | @Override 39 | protected void onCreate(@Nullable Bundle savedInstanceState) { 40 | setTheme(R.style.Theme_AppCompat_Light_NoActionBar); 41 | 42 | super.onCreate(savedInstanceState); 43 | 44 | ViewGroup root = createRootView(); 45 | scrollView = createScrollViewIn(root); 46 | 47 | setContentView(root); 48 | 49 | adapter = new TestAdapter(generateTestData(10)); 50 | scrollView.setAdapter(adapter); 51 | scrollView.addScrollStateChangeListener(this); 52 | } 53 | 54 | public DiscreteScrollView getScrollView() { 55 | return scrollView; 56 | } 57 | 58 | public TestAdapter getAdapter() { 59 | return adapter; 60 | } 61 | 62 | private DiscreteScrollView createScrollViewIn(ViewGroup root) { 63 | DiscreteScrollView scrollView = new DiscreteScrollView(this); 64 | FrameLayout.LayoutParams scrollViewLp = new FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT); 65 | scrollViewLp.gravity = Gravity.CENTER; 66 | scrollView.setLayoutParams(scrollViewLp); 67 | root.addView(scrollView); 68 | return scrollView; 69 | } 70 | 71 | private ViewGroup createRootView() { 72 | FrameLayout root = new FrameLayout(this); 73 | root.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 74 | return root; 75 | } 76 | 77 | public void incrementExpectedScrollEndCalls() { 78 | expectedScrollEndCalls.increment(); 79 | } 80 | 81 | @Override 82 | public void onScrollStart(@NonNull RecyclerView.ViewHolder currentItemHolder, int adapterPosition) { 83 | } 84 | 85 | @Override 86 | public void onScrollEnd(@NonNull RecyclerView.ViewHolder currentItemHolder, int adapterPosition) { 87 | if (!expectedScrollEndCalls.isIdleNow()) { 88 | expectedScrollEndCalls.decrement(); 89 | } 90 | } 91 | 92 | @Override 93 | public void onScroll(float scrollPosition, int currentPosition, int newPosition, @Nullable RecyclerView.ViewHolder currentHolder, @Nullable RecyclerView.ViewHolder newCurrent) { 94 | 95 | } 96 | 97 | public @NonNull List getIdlingResources() { 98 | return Collections.singletonList(expectedScrollEndCalls); 99 | } 100 | 101 | private List generateTestData(int size) { 102 | List result = new ArrayList<>(size); 103 | for (int i = 0; i < size; i++) { 104 | result.add(new TestData()); 105 | } 106 | return result; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestAdapter.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.context; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | import android.widget.ImageView; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * Created by yarolegovich on 2/4/18. 14 | */ 15 | 16 | public class TestAdapter extends RecyclerView.Adapter { 17 | 18 | private List data; 19 | private RecyclerView recyclerView; 20 | 21 | public TestAdapter(List data) { 22 | this.data = data; 23 | } 24 | 25 | @Override 26 | public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { 27 | super.onAttachedToRecyclerView(recyclerView); 28 | this.recyclerView = recyclerView; 29 | } 30 | 31 | @NonNull 32 | @Override 33 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 34 | float dp = parent.getResources().getDisplayMetrics().density; 35 | ImageView iv = new ImageView(parent.getContext()); 36 | iv.setLayoutParams(new ViewGroup.LayoutParams((int) (180 * dp), (int) (256 * dp))); 37 | iv.setScaleType(ImageView.ScaleType.CENTER_CROP); 38 | return new ViewHolder(iv); 39 | } 40 | 41 | @Override 42 | public void onBindViewHolder(ViewHolder holder, int position) { 43 | TestData item = data.get(position); 44 | holder.image.setImageDrawable(item.image); 45 | } 46 | 47 | @Override 48 | public int getItemCount() { 49 | return data.size(); 50 | } 51 | 52 | public List getData() { 53 | return data; 54 | } 55 | 56 | class ViewHolder extends RecyclerView.ViewHolder { 57 | 58 | public final ImageView image; 59 | 60 | public ViewHolder(View itemView) { 61 | super(itemView); 62 | image = (ImageView) itemView; 63 | itemView.setOnClickListener(new View.OnClickListener() { 64 | @Override 65 | public void onClick(View v) { 66 | recyclerView.smoothScrollToPosition(getAdapterPosition()); 67 | } 68 | }); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/yarolegovich/discretescrollview/context/TestData.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.context; 2 | 3 | import android.graphics.Color; 4 | import android.graphics.drawable.ColorDrawable; 5 | import android.graphics.drawable.Drawable; 6 | 7 | import androidx.annotation.ColorInt; 8 | 9 | import java.util.Random; 10 | 11 | /** 12 | * Created by yarolegovich on 2/4/18. 13 | */ 14 | 15 | public class TestData { 16 | 17 | private static int NEXT_ID = 1; 18 | private static final Random random = new Random(); 19 | 20 | public final int id; 21 | public final Drawable image; 22 | public TestData() { 23 | id = NEXT_ID++; 24 | image = new ColorDrawable(generateRandomColor()); 25 | } 26 | 27 | private static @ColorInt 28 | int generateRandomColor() { 29 | return Color.argb(255, 30 | random.nextInt(256), 31 | random.nextInt(256), 32 | random.nextInt(256)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/yarolegovich/discretescrollview/custom/CustomAssertions.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.custom; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.recyclerview.widget.RecyclerView; 7 | import androidx.test.espresso.NoMatchingViewException; 8 | import androidx.test.espresso.ViewAssertion; 9 | 10 | import com.yarolegovich.discretescrollview.DiscreteScrollView; 11 | 12 | import static org.hamcrest.Matchers.*; 13 | import static androidx.test.espresso.matcher.ViewMatchers.*; 14 | 15 | 16 | /** 17 | * Created by yarolegovich on 2/3/18. 18 | */ 19 | 20 | public class CustomAssertions { 21 | 22 | public static ViewAssertion currentPositionIs(final int expectedPosition) { 23 | return new ViewAssertion() { 24 | @Override 25 | public void check(View view, NoMatchingViewException noViewFoundException) { 26 | ensureViewFound(noViewFoundException); 27 | assertThat(view, isAssignableFrom(DiscreteScrollView.class)); 28 | DiscreteScrollView dsv = (DiscreteScrollView) view; 29 | assertThat(dsv.getCurrentItem(), is(equalTo(expectedPosition))); 30 | 31 | View midChild = findCenteredChildIn(dsv); 32 | assertThat(midChild, is(notNullValue())); 33 | RecyclerView.ViewHolder holder = dsv.getChildViewHolder(midChild); 34 | assertThat(holder.getAdapterPosition(), is(equalTo(expectedPosition))); 35 | } 36 | }; 37 | } 38 | 39 | public static ViewAssertion doesNotHaveChildren() { 40 | return new ViewAssertion() { 41 | @Override 42 | public void check(View view, NoMatchingViewException noViewFoundException) { 43 | ensureViewFound(noViewFoundException); 44 | assertThat(view, isAssignableFrom(ViewGroup.class)); 45 | ViewGroup viewGroup = (ViewGroup) view; 46 | assertThat(viewGroup.getChildCount(), is(equalTo(0))); 47 | } 48 | }; 49 | } 50 | 51 | private static View findCenteredChildIn(DiscreteScrollView dsv) { 52 | final int centerX = dsv.getWidth() / 2; 53 | final int centerY = dsv.getHeight() / 2; 54 | for (int i = 0; i < dsv.getChildCount(); i++) { 55 | View child = dsv.getChildAt(i); 56 | if (centerX == (child.getLeft() + child.getWidth() / 2) 57 | && centerY == (child.getTop() + child.getHeight() / 2)) { 58 | return child; 59 | } 60 | } 61 | throw new AssertionError("can't find centered child"); 62 | } 63 | 64 | private static boolean isMidpoint(int value, int rangeStart, int rangeEnd) { 65 | return value == (rangeStart + rangeEnd) / 2; 66 | } 67 | 68 | private static void ensureViewFound(NoMatchingViewException exception) { 69 | if (exception != null) { 70 | throw exception; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/DSVOrientation.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | import android.graphics.Point; 4 | import android.view.View; 5 | 6 | /** 7 | * Created by yarolegovich on 16.03.2017. 8 | */ 9 | public enum DSVOrientation { 10 | 11 | HORIZONTAL { 12 | @Override 13 | Helper createHelper() { 14 | return new HorizontalHelper(); 15 | } 16 | }, 17 | VERTICAL { 18 | @Override 19 | Helper createHelper() { 20 | return new VerticalHelper(); 21 | } 22 | }; 23 | 24 | //Package private 25 | abstract Helper createHelper(); 26 | 27 | interface Helper { 28 | 29 | int getViewEnd(int recyclerWidth, int recyclerHeight); 30 | 31 | int getDistanceToChangeCurrent(int childWidth, int childHeight); 32 | 33 | void setCurrentViewCenter(Point recyclerCenter, int scrolled, Point outPoint); 34 | 35 | void shiftViewCenter(Direction direction, int shiftAmount, Point outCenter); 36 | 37 | int getFlingVelocity(int velocityX, int velocityY); 38 | 39 | int getPendingDx(int pendingScroll); 40 | 41 | int getPendingDy(int pendingScroll); 42 | 43 | void offsetChildren(int amount, RecyclerViewProxy lm); 44 | 45 | float getDistanceFromCenter(Point center, float viewCenterX, float viewCenterY); 46 | 47 | boolean isViewVisible(Point center, int halfWidth, int halfHeight, int endBound, int extraSpace); 48 | 49 | boolean hasNewBecomeVisible(DiscreteScrollLayoutManager lm); 50 | 51 | boolean canScrollVertically(); 52 | 53 | boolean canScrollHorizontally(); 54 | } 55 | 56 | protected static class HorizontalHelper implements Helper { 57 | 58 | @Override 59 | public int getViewEnd(int recyclerWidth, int recyclerHeight) { 60 | return recyclerWidth; 61 | } 62 | 63 | @Override 64 | public int getDistanceToChangeCurrent(int childWidth, int childHeight) { 65 | return childWidth; 66 | } 67 | 68 | @Override 69 | public void setCurrentViewCenter(Point recyclerCenter, int scrolled, Point outPoint) { 70 | int newX = recyclerCenter.x - scrolled; 71 | outPoint.set(newX, recyclerCenter.y); 72 | } 73 | 74 | @Override 75 | public void shiftViewCenter(Direction direction, int shiftAmount, Point outCenter) { 76 | int newX = outCenter.x + direction.applyTo(shiftAmount); 77 | outCenter.set(newX, outCenter.y); 78 | } 79 | 80 | @Override 81 | public boolean isViewVisible( 82 | Point viewCenter, int halfWidth, int halfHeight, int endBound, 83 | int extraSpace) { 84 | int viewLeft = viewCenter.x - halfWidth; 85 | int viewRight = viewCenter.x + halfWidth; 86 | return viewLeft < (endBound + extraSpace) && viewRight > -extraSpace; 87 | } 88 | 89 | @Override 90 | public boolean hasNewBecomeVisible(DiscreteScrollLayoutManager lm) { 91 | View firstChild = lm.getFirstChild(), lastChild = lm.getLastChild(); 92 | int leftBound = -lm.getExtraLayoutSpace(); 93 | int rightBound = lm.getWidth() + lm.getExtraLayoutSpace(); 94 | boolean isNewVisibleFromLeft = lm.getDecoratedLeft(firstChild) > leftBound 95 | && lm.getPosition(firstChild) > 0; 96 | boolean isNewVisibleFromRight = lm.getDecoratedRight(lastChild) < rightBound 97 | && lm.getPosition(lastChild) < lm.getItemCount() - 1; 98 | return isNewVisibleFromLeft || isNewVisibleFromRight; 99 | } 100 | 101 | @Override 102 | public void offsetChildren(int amount, RecyclerViewProxy helper) { 103 | helper.offsetChildrenHorizontal(amount); 104 | } 105 | 106 | @Override 107 | public float getDistanceFromCenter(Point center, float viewCenterX, float viewCenterY) { 108 | return viewCenterX - center.x; 109 | } 110 | 111 | @Override 112 | public int getFlingVelocity(int velocityX, int velocityY) { 113 | return velocityX; 114 | } 115 | 116 | @Override 117 | public boolean canScrollHorizontally() { 118 | return true; 119 | } 120 | 121 | @Override 122 | public boolean canScrollVertically() { 123 | return false; 124 | } 125 | 126 | @Override 127 | public int getPendingDx(int pendingScroll) { 128 | return pendingScroll; 129 | } 130 | 131 | @Override 132 | public int getPendingDy(int pendingScroll) { 133 | return 0; 134 | } 135 | } 136 | 137 | 138 | protected static class VerticalHelper implements Helper { 139 | 140 | @Override 141 | public int getViewEnd(int recyclerWidth, int recyclerHeight) { 142 | return recyclerHeight; 143 | } 144 | 145 | @Override 146 | public int getDistanceToChangeCurrent(int childWidth, int childHeight) { 147 | return childHeight; 148 | } 149 | 150 | @Override 151 | public void setCurrentViewCenter(Point recyclerCenter, int scrolled, Point outPoint) { 152 | int newY = recyclerCenter.y - scrolled; 153 | outPoint.set(recyclerCenter.x, newY); 154 | } 155 | 156 | @Override 157 | public void shiftViewCenter(Direction direction, int shiftAmount, Point outCenter) { 158 | int newY = outCenter.y + direction.applyTo(shiftAmount); 159 | outCenter.set(outCenter.x, newY); 160 | } 161 | 162 | @Override 163 | public void offsetChildren(int amount, RecyclerViewProxy helper) { 164 | helper.offsetChildrenVertical(amount); 165 | } 166 | 167 | @Override 168 | public float getDistanceFromCenter(Point center, float viewCenterX, float viewCenterY) { 169 | return viewCenterY - center.y; 170 | } 171 | 172 | @Override 173 | public boolean isViewVisible( 174 | Point viewCenter, int halfWidth, int halfHeight, int endBound, 175 | int extraSpace) { 176 | int viewTop = viewCenter.y - halfHeight; 177 | int viewBottom = viewCenter.y + halfHeight; 178 | return viewTop < (endBound + extraSpace) && viewBottom > -extraSpace; 179 | } 180 | 181 | @Override 182 | public boolean hasNewBecomeVisible(DiscreteScrollLayoutManager lm) { 183 | View firstChild = lm.getFirstChild(), lastChild = lm.getLastChild(); 184 | int topBound = -lm.getExtraLayoutSpace(); 185 | int bottomBound = lm.getHeight() + lm.getExtraLayoutSpace(); 186 | boolean isNewVisibleFromTop = lm.getDecoratedTop(firstChild) > topBound 187 | && lm.getPosition(firstChild) > 0; 188 | boolean isNewVisibleFromBottom = lm.getDecoratedBottom(lastChild) < bottomBound 189 | && lm.getPosition(lastChild) < lm.getItemCount() - 1; 190 | return isNewVisibleFromTop || isNewVisibleFromBottom; 191 | } 192 | 193 | @Override 194 | public int getFlingVelocity(int velocityX, int velocityY) { 195 | return velocityY; 196 | } 197 | 198 | @Override 199 | public boolean canScrollHorizontally() { 200 | return false; 201 | } 202 | 203 | @Override 204 | public boolean canScrollVertically() { 205 | return true; 206 | } 207 | 208 | @Override 209 | public int getPendingDx(int pendingScroll) { 210 | return 0; 211 | } 212 | 213 | @Override 214 | public int getPendingDy(int pendingScroll) { 215 | return pendingScroll; 216 | } 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/DSVScrollConfig.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | public enum DSVScrollConfig { 4 | ENABLED { 5 | @Override 6 | boolean isScrollBlocked(Direction direction) { 7 | return false; 8 | } 9 | }, 10 | FORWARD_ONLY { 11 | @Override 12 | boolean isScrollBlocked(Direction direction) { 13 | return direction == Direction.START; 14 | } 15 | }, 16 | BACKWARD_ONLY { 17 | @Override 18 | boolean isScrollBlocked(Direction direction) { 19 | return direction == Direction.END; 20 | } 21 | }, 22 | DISABLED { 23 | @Override 24 | boolean isScrollBlocked(Direction direction) { 25 | return true; 26 | } 27 | }; 28 | 29 | abstract boolean isScrollBlocked(Direction direction); 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/Direction.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | /** 4 | * Created by yarolegovich on 16.03.2017. 5 | */ 6 | enum Direction { 7 | 8 | START { 9 | @Override 10 | public int applyTo(int delta) { 11 | return delta * -1; 12 | } 13 | 14 | @Override 15 | public boolean sameAs(int direction) { 16 | return direction < 0; 17 | } 18 | 19 | @Override 20 | public Direction reverse() { 21 | return Direction.END; 22 | } 23 | }, 24 | END { 25 | @Override 26 | public int applyTo(int delta) { 27 | return delta; 28 | } 29 | 30 | @Override 31 | public boolean sameAs(int direction) { 32 | return direction > 0; 33 | } 34 | 35 | @Override 36 | public Direction reverse() { 37 | return Direction.START; 38 | } 39 | }; 40 | 41 | public abstract int applyTo(int delta); 42 | 43 | public abstract boolean sameAs(int direction); 44 | 45 | public abstract Direction reverse(); 46 | 47 | public static Direction fromDelta(int delta) { 48 | return delta > 0 ? END : START; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/DiscreteScrollView.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.AttributeSet; 6 | import android.view.View; 7 | 8 | import androidx.annotation.IntRange; 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.recyclerview.widget.RecyclerView; 12 | 13 | import com.yarolegovich.discretescrollview.transform.DiscreteScrollItemTransformer; 14 | import com.yarolegovich.discretescrollview.util.ScrollListenerAdapter; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | /** 20 | * Created by yarolegovich on 18.02.2017. 21 | */ 22 | @SuppressWarnings({"unchecked", "rawtypes"}) 23 | public class DiscreteScrollView extends RecyclerView { 24 | 25 | public static final int NO_POSITION = DiscreteScrollLayoutManager.NO_POSITION; 26 | 27 | private static final int DEFAULT_ORIENTATION = DSVOrientation.HORIZONTAL.ordinal(); 28 | 29 | private DiscreteScrollLayoutManager layoutManager; 30 | 31 | private List scrollStateChangeListeners; 32 | private List onItemChangedListeners; 33 | private Runnable notifyItemChangedRunnable = new Runnable() { 34 | @Override 35 | public void run() { 36 | notifyCurrentItemChanged(); 37 | } 38 | }; 39 | 40 | private boolean isOverScrollEnabled; 41 | 42 | public DiscreteScrollView(Context context) { 43 | super(context); 44 | init(null); 45 | } 46 | 47 | public DiscreteScrollView(Context context, AttributeSet attrs) { 48 | super(context, attrs); 49 | init(attrs); 50 | } 51 | 52 | public DiscreteScrollView(Context context, AttributeSet attrs, int defStyleAttr) { 53 | super(context, attrs, defStyleAttr); 54 | init(attrs); 55 | } 56 | 57 | private void init(AttributeSet attrs) { 58 | scrollStateChangeListeners = new ArrayList<>(); 59 | onItemChangedListeners = new ArrayList<>(); 60 | 61 | int orientation = DEFAULT_ORIENTATION; 62 | if (attrs != null) { 63 | TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.DiscreteScrollView); 64 | orientation = ta.getInt(R.styleable.DiscreteScrollView_dsv_orientation, DEFAULT_ORIENTATION); 65 | ta.recycle(); 66 | } 67 | 68 | isOverScrollEnabled = getOverScrollMode() != OVER_SCROLL_NEVER; 69 | 70 | layoutManager = new DiscreteScrollLayoutManager( 71 | getContext(), new ScrollStateListener(), 72 | DSVOrientation.values()[orientation]); 73 | setLayoutManager(layoutManager); 74 | } 75 | 76 | @Override 77 | public void setLayoutManager(LayoutManager layout) { 78 | if (layout instanceof DiscreteScrollLayoutManager) { 79 | super.setLayoutManager(layout); 80 | } else { 81 | throw new IllegalArgumentException(getContext().getString(R.string.dsv_ex_msg_dont_set_lm)); 82 | } 83 | } 84 | 85 | 86 | @Override 87 | public boolean fling(int velocityX, int velocityY) { 88 | if (layoutManager.isFlingDisallowed(velocityX, velocityY)) { 89 | return false; 90 | } 91 | boolean isFling = super.fling(velocityX, velocityY); 92 | if (isFling) { 93 | layoutManager.onFling(velocityX, velocityY); 94 | } else { 95 | layoutManager.returnToCurrentPosition(); 96 | } 97 | return isFling; 98 | } 99 | 100 | @Nullable 101 | public ViewHolder getViewHolder(int position) { 102 | View view = layoutManager.findViewByPosition(position); 103 | return view != null ? getChildViewHolder(view) : null; 104 | } 105 | 106 | @Override 107 | public void scrollToPosition(int position) { 108 | int currentPosition = layoutManager.getCurrentPosition(); 109 | super.scrollToPosition(position); 110 | if (currentPosition != position) { 111 | notifyCurrentItemChanged(); 112 | } 113 | } 114 | 115 | /** 116 | * @return adapter position of the current item or -1 if nothing is selected 117 | */ 118 | public int getCurrentItem() { 119 | return layoutManager.getCurrentPosition(); 120 | } 121 | 122 | public void setItemTransformer(DiscreteScrollItemTransformer transformer) { 123 | layoutManager.setItemTransformer(transformer); 124 | } 125 | 126 | public void setItemTransitionTimeMillis(@IntRange(from = 10) int millis) { 127 | layoutManager.setTimeForItemSettle(millis); 128 | } 129 | 130 | public void setSlideOnFling(boolean result){ 131 | layoutManager.setShouldSlideOnFling(result); 132 | } 133 | 134 | public void setSlideOnFlingThreshold(int threshold){ 135 | layoutManager.setSlideOnFlingThreshold(threshold); 136 | } 137 | 138 | public void setOrientation(DSVOrientation orientation) { 139 | layoutManager.setOrientation(orientation); 140 | } 141 | 142 | public void setOffscreenItems(int items) { 143 | layoutManager.setOffscreenItems(items); 144 | } 145 | 146 | public void setScrollConfig(@NonNull DSVScrollConfig config) { 147 | layoutManager.setScrollConfig(config); 148 | } 149 | 150 | public void setClampTransformProgressAfter(@IntRange(from = 1) int itemCount) { 151 | if (itemCount <= 1) { 152 | throw new IllegalArgumentException("must be >= 1"); 153 | } 154 | layoutManager.setTransformClampItemCount(itemCount); 155 | } 156 | 157 | public void setOverScrollEnabled(boolean overScrollEnabled) { 158 | isOverScrollEnabled = overScrollEnabled; 159 | setOverScrollMode(OVER_SCROLL_NEVER); 160 | } 161 | 162 | public void addScrollStateChangeListener(@NonNull ScrollStateChangeListener scrollStateChangeListener) { 163 | scrollStateChangeListeners.add(scrollStateChangeListener); 164 | } 165 | 166 | public void addScrollListener(@NonNull ScrollListener scrollListener) { 167 | addScrollStateChangeListener(new ScrollListenerAdapter(scrollListener)); 168 | } 169 | 170 | public void addOnItemChangedListener(@NonNull OnItemChangedListener onItemChangedListener) { 171 | onItemChangedListeners.add(onItemChangedListener); 172 | } 173 | 174 | public void removeScrollStateChangeListener(@NonNull ScrollStateChangeListener scrollStateChangeListener) { 175 | scrollStateChangeListeners.remove(scrollStateChangeListener); 176 | } 177 | 178 | public void removeScrollListener(@NonNull ScrollListener scrollListener) { 179 | removeScrollStateChangeListener(new ScrollListenerAdapter<>(scrollListener)); 180 | } 181 | 182 | public void removeItemChangedListener(@NonNull OnItemChangedListener onItemChangedListener) { 183 | onItemChangedListeners.remove(onItemChangedListener); 184 | } 185 | 186 | private void notifyScrollStart(ViewHolder holder, int current) { 187 | for (ScrollStateChangeListener listener : scrollStateChangeListeners) { 188 | listener.onScrollStart(holder, current); 189 | } 190 | } 191 | 192 | private void notifyScrollEnd(ViewHolder holder, int current) { 193 | for (ScrollStateChangeListener listener : scrollStateChangeListeners) { 194 | listener.onScrollEnd(holder, current); 195 | } 196 | } 197 | 198 | private void notifyScroll(float position, 199 | int currentIndex, int newIndex, 200 | ViewHolder currentHolder, ViewHolder newHolder) { 201 | for (ScrollStateChangeListener listener : scrollStateChangeListeners) { 202 | listener.onScroll(position, currentIndex, newIndex, 203 | currentHolder, 204 | newHolder); 205 | } 206 | } 207 | 208 | private void notifyCurrentItemChanged(ViewHolder holder, int current) { 209 | for (OnItemChangedListener listener : onItemChangedListeners) { 210 | listener.onCurrentItemChanged(holder, current); 211 | } 212 | } 213 | 214 | private void notifyCurrentItemChanged() { 215 | removeCallbacks(notifyItemChangedRunnable); 216 | if (onItemChangedListeners.isEmpty()) { 217 | return; 218 | } 219 | int current = layoutManager.getCurrentPosition(); 220 | ViewHolder currentHolder = getViewHolder(current); 221 | if (currentHolder == null) { 222 | post(notifyItemChangedRunnable); 223 | } else { 224 | notifyCurrentItemChanged(currentHolder, current); 225 | } 226 | } 227 | 228 | private class ScrollStateListener implements DiscreteScrollLayoutManager.ScrollStateListener { 229 | 230 | @Override 231 | public void onIsBoundReachedFlagChange(boolean isBoundReached) { 232 | if (isOverScrollEnabled) { 233 | setOverScrollMode(isBoundReached ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER); 234 | } 235 | } 236 | 237 | @Override 238 | public void onScrollStart() { 239 | removeCallbacks(notifyItemChangedRunnable); 240 | if (scrollStateChangeListeners.isEmpty()) { 241 | return; 242 | } 243 | int current = layoutManager.getCurrentPosition(); 244 | ViewHolder holder = getViewHolder(current); 245 | if (holder != null) { 246 | notifyScrollStart(holder, current); 247 | } 248 | } 249 | 250 | @Override 251 | public void onScrollEnd() { 252 | if (onItemChangedListeners.isEmpty() && scrollStateChangeListeners.isEmpty()) { 253 | return; 254 | } 255 | int current = layoutManager.getCurrentPosition(); 256 | ViewHolder holder = getViewHolder(current); 257 | if (holder != null) { 258 | notifyScrollEnd(holder, current); 259 | notifyCurrentItemChanged(holder, current); 260 | } 261 | } 262 | 263 | @Override 264 | public void onScroll(float currentViewPosition) { 265 | if (scrollStateChangeListeners.isEmpty()) { 266 | return; 267 | } 268 | int currentIndex = getCurrentItem(); 269 | int newIndex = layoutManager.getNextPosition(); 270 | if (currentIndex != newIndex) { 271 | notifyScroll(currentViewPosition, 272 | currentIndex, newIndex, 273 | getViewHolder(currentIndex), 274 | getViewHolder(newIndex)); 275 | } 276 | } 277 | 278 | @Override 279 | public void onCurrentViewFirstLayout() { 280 | notifyCurrentItemChanged(); 281 | } 282 | 283 | @Override 284 | public void onDataSetChangeChangedPosition() { 285 | notifyCurrentItemChanged(); 286 | } 287 | } 288 | 289 | public interface ScrollStateChangeListener { 290 | 291 | void onScrollStart(@NonNull T currentItemHolder, int adapterPosition); 292 | 293 | void onScrollEnd(@NonNull T currentItemHolder, int adapterPosition); 294 | 295 | void onScroll(float scrollPosition, 296 | int currentPosition, 297 | int newPosition, 298 | @Nullable T currentHolder, 299 | @Nullable T newCurrent); 300 | } 301 | 302 | public interface ScrollListener { 303 | 304 | void onScroll(float scrollPosition, 305 | int currentPosition, int newPosition, 306 | @Nullable T currentHolder, 307 | @Nullable T newCurrent); 308 | } 309 | 310 | public interface OnItemChangedListener { 311 | /* 312 | * This method will be also triggered when view appears on the screen for the first time. 313 | * If data set is empty, viewHolder will be null and adapterPosition will be NO_POSITION 314 | */ 315 | void onCurrentItemChanged(@Nullable T viewHolder, int adapterPosition); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/InfiniteScrollAdapter.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | import android.view.ViewGroup; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.recyclerview.widget.RecyclerView; 7 | 8 | import java.util.Locale; 9 | 10 | /** 11 | * Created by yarolegovich on 28-Apr-17. 12 | */ 13 | 14 | public class InfiniteScrollAdapter extends RecyclerView.Adapter 15 | implements DiscreteScrollLayoutManager.InitialPositionProvider { 16 | 17 | private static final int CENTER = Integer.MAX_VALUE / 2; 18 | private static final int RESET_BOUND = 100; 19 | 20 | public static InfiniteScrollAdapter wrap( 21 | @NonNull RecyclerView.Adapter adapter) { 22 | return new InfiniteScrollAdapter<>(adapter); 23 | } 24 | 25 | private RecyclerView.Adapter wrapped; 26 | private DiscreteScrollLayoutManager layoutManager; 27 | 28 | public InfiniteScrollAdapter(@NonNull RecyclerView.Adapter wrapped) { 29 | this.wrapped = wrapped; 30 | this.wrapped.registerAdapterDataObserver(new DataSetChangeDelegate()); 31 | } 32 | 33 | @Override 34 | public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { 35 | wrapped.onAttachedToRecyclerView(recyclerView); 36 | if (recyclerView instanceof DiscreteScrollView) { 37 | layoutManager = (DiscreteScrollLayoutManager) recyclerView.getLayoutManager(); 38 | } else { 39 | String msg = recyclerView.getContext().getString(R.string.dsv_ex_msg_adapter_wrong_recycler); 40 | throw new RuntimeException(msg); 41 | } 42 | } 43 | 44 | @Override 45 | public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { 46 | wrapped.onDetachedFromRecyclerView(recyclerView); 47 | layoutManager = null; 48 | } 49 | 50 | @Override 51 | public @NonNull T onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 52 | return wrapped.onCreateViewHolder(parent, viewType); 53 | } 54 | 55 | @Override 56 | public void onBindViewHolder(@NonNull T holder, int position) { 57 | if (isResetRequired(position)) { 58 | int resetPosition = CENTER + mapPositionToReal(layoutManager.getCurrentPosition()); 59 | setPosition(resetPosition); 60 | return; 61 | } 62 | wrapped.onBindViewHolder(holder, mapPositionToReal(position)); 63 | } 64 | 65 | @Override 66 | public int getItemViewType(int position) { 67 | return wrapped.getItemViewType(mapPositionToReal(position)); 68 | } 69 | 70 | @Override 71 | public int getItemCount() { 72 | return isInfinite() ? Integer.MAX_VALUE : wrapped.getItemCount(); 73 | } 74 | 75 | public int getRealItemCount() { 76 | return wrapped.getItemCount(); 77 | } 78 | 79 | public int getRealCurrentPosition() { 80 | return getRealPosition(layoutManager.getCurrentPosition()); 81 | } 82 | 83 | public int getRealPosition(int position) { 84 | return mapPositionToReal(position); 85 | } 86 | 87 | public int getClosestPosition(int position) { 88 | ensureValidPosition(position); 89 | int adapterCurrent = layoutManager.getCurrentPosition(); 90 | int current = mapPositionToReal(adapterCurrent); 91 | if (position == current) { 92 | return adapterCurrent; 93 | } 94 | int delta = position - current; 95 | int target = adapterCurrent + delta; 96 | int wraparoundTarget = adapterCurrent + (position > current ? 97 | delta - wrapped.getItemCount() : 98 | wrapped.getItemCount() + delta); 99 | int distance = Math.abs(adapterCurrent - target); 100 | int wraparoundDistance = Math.abs(adapterCurrent - wraparoundTarget); 101 | if (distance == wraparoundDistance) { 102 | //Scroll to the right feels more natural, so prefer it 103 | return target > adapterCurrent ? target : wraparoundTarget; 104 | } else { 105 | return distance < wraparoundDistance ? target : wraparoundTarget; 106 | } 107 | } 108 | 109 | private int mapPositionToReal(int position) { 110 | if (position < CENTER) { 111 | int rem = (CENTER - position) % wrapped.getItemCount(); 112 | return rem == 0 ? 0 : wrapped.getItemCount() - rem; 113 | } else { 114 | return (position - CENTER) % wrapped.getItemCount(); 115 | } 116 | } 117 | 118 | private boolean isResetRequired(int requestedPosition) { 119 | return isInfinite() 120 | && (requestedPosition <= RESET_BOUND 121 | || requestedPosition >= (Integer.MAX_VALUE - RESET_BOUND)); 122 | } 123 | 124 | private void ensureValidPosition(int position) { 125 | if (position >= wrapped.getItemCount()) { 126 | throw new IndexOutOfBoundsException(String.format(Locale.US, 127 | "requested position is outside adapter's bounds: position=%d, size=%d", 128 | position, wrapped.getItemCount())); 129 | } 130 | } 131 | 132 | private boolean isInfinite() { 133 | return wrapped.getItemCount() > 1; 134 | } 135 | 136 | @Override 137 | public int getInitialPosition() { 138 | return isInfinite() ? CENTER : 0; 139 | } 140 | 141 | private void setPosition(int position) { 142 | layoutManager.scrollToPosition(position); 143 | } 144 | 145 | //TODO: handle proper data set change notifications 146 | private class DataSetChangeDelegate extends RecyclerView.AdapterDataObserver { 147 | 148 | @Override 149 | public void onChanged() { 150 | setPosition(getInitialPosition()); 151 | notifyDataSetChanged(); 152 | } 153 | 154 | @Override 155 | public void onItemRangeRemoved(int positionStart, int itemCount) { 156 | onChanged(); 157 | } 158 | 159 | @Override 160 | public void onItemRangeInserted(int positionStart, int itemCount) { 161 | onChanged(); 162 | } 163 | 164 | @Override 165 | public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { 166 | onChanged(); 167 | } 168 | 169 | @Override 170 | public void onItemRangeChanged(int positionStart, int itemCount) { 171 | notifyItemRangeChanged(0, getItemCount()); 172 | } 173 | 174 | @Override 175 | public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { 176 | notifyItemRangeChanged(0, getItemCount(), payload); 177 | } 178 | } 179 | } -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/RecyclerViewProxy.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.recyclerview.widget.RecyclerView; 8 | 9 | /** 10 | * Created by yarolegovich on 10/25/17. 11 | */ 12 | public class RecyclerViewProxy { 13 | 14 | private RecyclerView.LayoutManager layoutManager; 15 | 16 | public RecyclerViewProxy(@NonNull RecyclerView.LayoutManager layoutManager) { 17 | this.layoutManager = layoutManager; 18 | } 19 | 20 | public void attachView(View view) { 21 | layoutManager.attachView(view); 22 | } 23 | 24 | public void detachView(View view) { 25 | layoutManager.detachView(view); 26 | } 27 | 28 | public void detachAndScrapView(View view, RecyclerView.Recycler recycler) { 29 | layoutManager.detachAndScrapView(view, recycler); 30 | } 31 | 32 | public void detachAndScrapAttachedViews(RecyclerView.Recycler recycler) { 33 | layoutManager.detachAndScrapAttachedViews(recycler); 34 | } 35 | 36 | public void recycleView(View view, RecyclerView.Recycler recycler) { 37 | recycler.recycleView(view); 38 | } 39 | 40 | public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) { 41 | layoutManager.removeAndRecycleAllViews(recycler); 42 | } 43 | 44 | public int getChildCount() { 45 | return layoutManager.getChildCount(); 46 | } 47 | 48 | public int getItemCount() { 49 | return layoutManager.getItemCount(); 50 | } 51 | 52 | public View getMeasuredChildForAdapterPosition(int position, RecyclerView.Recycler recycler) { 53 | View view = recycler.getViewForPosition(position); 54 | layoutManager.addView(view); 55 | layoutManager.measureChildWithMargins(view, 0, 0); 56 | return view; 57 | } 58 | 59 | public void layoutDecoratedWithMargins(View v, int left, int top, int right, int bottom) { 60 | layoutManager.layoutDecoratedWithMargins(v, left, top, right, bottom); 61 | } 62 | 63 | public View getChildAt(int index) { 64 | return layoutManager.getChildAt(index); 65 | } 66 | 67 | public int getPosition(View view) { 68 | return layoutManager.getPosition(view); 69 | } 70 | 71 | public int getMeasuredWidthWithMargin(View child) { 72 | ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams(); 73 | return layoutManager.getDecoratedMeasuredWidth(child) + lp.leftMargin + lp.rightMargin; 74 | } 75 | 76 | public int getMeasuredHeightWithMargin(View child) { 77 | ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams(); 78 | return layoutManager.getDecoratedMeasuredHeight(child) + lp.topMargin + lp.bottomMargin; 79 | } 80 | 81 | public int getWidth() { 82 | return layoutManager.getWidth(); 83 | } 84 | 85 | public int getHeight() { 86 | return layoutManager.getHeight(); 87 | } 88 | 89 | public void offsetChildrenHorizontal(int amount) { 90 | layoutManager.offsetChildrenHorizontal(amount); 91 | } 92 | 93 | public void offsetChildrenVertical(int amount) { 94 | layoutManager.offsetChildrenVertical(amount); 95 | } 96 | 97 | public void requestLayout() { 98 | layoutManager.requestLayout(); 99 | } 100 | 101 | public void startSmoothScroll(RecyclerView.SmoothScroller smoothScroller) { 102 | layoutManager.startSmoothScroll(smoothScroller); 103 | } 104 | 105 | public void removeAllViews() { 106 | layoutManager.removeAllViews(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/transform/DiscreteScrollItemTransformer.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.transform; 2 | 3 | import android.view.View; 4 | 5 | /** 6 | * Created by yarolegovich on 02.03.2017. 7 | */ 8 | 9 | public interface DiscreteScrollItemTransformer { 10 | void transformItem(View item, float position); 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/transform/Pivot.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.transform; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.IntDef; 6 | 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | 10 | /** 11 | * Created by yarolegovich on 03.03.2017. 12 | */ 13 | 14 | public class Pivot { 15 | 16 | public static final int AXIS_X = 0; 17 | public static final int AXIS_Y = 1; 18 | 19 | private static final int PIVOT_CENTER = -1; 20 | private static final int PIVOT_MAX = -2; 21 | 22 | private int axis; 23 | private int pivotPoint; 24 | 25 | public Pivot(@Axis int axis, int pivotPoint) { 26 | this.axis = axis; 27 | this.pivotPoint = pivotPoint; 28 | } 29 | 30 | public void setOn(View view) { 31 | if (axis == AXIS_X) { 32 | switch (pivotPoint) { 33 | case PIVOT_CENTER: 34 | view.setPivotX(view.getWidth() * 0.5f); 35 | break; 36 | case PIVOT_MAX: 37 | view.setPivotX(view.getWidth()); 38 | break; 39 | default: 40 | view.setPivotX(pivotPoint); 41 | break; 42 | } 43 | return; 44 | } 45 | 46 | if (axis == AXIS_Y) { 47 | switch (pivotPoint) { 48 | case PIVOT_CENTER: 49 | view.setPivotY(view.getHeight() * 0.5f); 50 | break; 51 | case PIVOT_MAX: 52 | view.setPivotY(view.getHeight()); 53 | break; 54 | default: 55 | view.setPivotY(pivotPoint); 56 | break; 57 | } 58 | } 59 | } 60 | 61 | @Axis 62 | public int getAxis() { 63 | return axis; 64 | } 65 | 66 | public enum X { 67 | LEFT { 68 | @Override 69 | public Pivot create() { 70 | return new Pivot(AXIS_X, 0); 71 | } 72 | }, 73 | CENTER { 74 | @Override 75 | public Pivot create() { 76 | return new Pivot(AXIS_X, PIVOT_CENTER); 77 | } 78 | }, 79 | RIGHT { 80 | @Override 81 | public Pivot create() { 82 | return new Pivot(AXIS_X, PIVOT_MAX); 83 | } 84 | }; 85 | 86 | public abstract Pivot create(); 87 | } 88 | 89 | public enum Y { 90 | TOP { 91 | @Override 92 | public Pivot create() { 93 | return new Pivot(AXIS_Y, 0); 94 | } 95 | }, 96 | CENTER { 97 | @Override 98 | public Pivot create() { 99 | return new Pivot(AXIS_Y, PIVOT_CENTER); 100 | } 101 | }, 102 | BOTTOM { 103 | @Override 104 | public Pivot create() { 105 | return new Pivot(AXIS_Y, PIVOT_MAX); 106 | } 107 | }; 108 | 109 | public abstract Pivot create(); 110 | } 111 | 112 | @IntDef({AXIS_X, AXIS_Y}) 113 | @Retention(RetentionPolicy.SOURCE) 114 | public @interface Axis{ 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/transform/ScaleTransformer.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.transform; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.FloatRange; 6 | 7 | /** 8 | * Created by yarolegovich on 03.03.2017. 9 | */ 10 | public class ScaleTransformer implements DiscreteScrollItemTransformer { 11 | 12 | private Pivot pivotX; 13 | private Pivot pivotY; 14 | private float minScale; 15 | private float maxMinDiff; 16 | 17 | public ScaleTransformer() { 18 | pivotX = Pivot.X.CENTER.create(); 19 | pivotY = Pivot.Y.CENTER.create(); 20 | minScale = 0.8f; 21 | maxMinDiff = 0.2f; 22 | } 23 | 24 | @Override 25 | public void transformItem(View item, float position) { 26 | pivotX.setOn(item); 27 | pivotY.setOn(item); 28 | float closenessToCenter = 1f - Math.abs(position); 29 | float scale = minScale + maxMinDiff * closenessToCenter; 30 | item.setScaleX(scale); 31 | item.setScaleY(scale); 32 | } 33 | 34 | public static class Builder { 35 | 36 | private ScaleTransformer transformer; 37 | private float maxScale; 38 | 39 | public Builder() { 40 | transformer = new ScaleTransformer(); 41 | maxScale = 1f; 42 | } 43 | 44 | public Builder setMinScale(@FloatRange(from = 0.01) float scale) { 45 | transformer.minScale = scale; 46 | return this; 47 | } 48 | 49 | public Builder setMaxScale(@FloatRange(from = 0.01) float scale) { 50 | maxScale = scale; 51 | return this; 52 | } 53 | 54 | public Builder setPivotX(Pivot.X pivotX) { 55 | return setPivotX(pivotX.create()); 56 | } 57 | 58 | public Builder setPivotX(Pivot pivot) { 59 | assertAxis(pivot, Pivot.AXIS_X); 60 | transformer.pivotX = pivot; 61 | return this; 62 | } 63 | 64 | public Builder setPivotY(Pivot.Y pivotY) { 65 | return setPivotY(pivotY.create()); 66 | } 67 | 68 | public Builder setPivotY(Pivot pivot) { 69 | assertAxis(pivot, Pivot.AXIS_Y); 70 | transformer.pivotY = pivot; 71 | return this; 72 | } 73 | 74 | public ScaleTransformer build() { 75 | transformer.maxMinDiff = maxScale - transformer.minScale; 76 | return transformer; 77 | } 78 | 79 | private void assertAxis(Pivot pivot, @Pivot.Axis int axis) { 80 | if (pivot.getAxis() != axis) { 81 | throw new IllegalArgumentException("You passed a Pivot for wrong axis."); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /library/src/main/java/com/yarolegovich/discretescrollview/util/ScrollListenerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.util; 2 | 3 | 4 | import androidx.annotation.NonNull; 5 | import androidx.annotation.Nullable; 6 | import androidx.recyclerview.widget.RecyclerView; 7 | 8 | import com.yarolegovich.discretescrollview.DiscreteScrollView; 9 | 10 | /** 11 | * Created by yarolegovich on 16.03.2017. 12 | */ 13 | public class ScrollListenerAdapter implements DiscreteScrollView.ScrollStateChangeListener { 14 | 15 | private DiscreteScrollView.ScrollListener adaptee; 16 | 17 | public ScrollListenerAdapter(@NonNull DiscreteScrollView.ScrollListener adaptee) { 18 | this.adaptee = adaptee; 19 | } 20 | 21 | @Override 22 | public void onScrollStart(@NonNull T currentItemHolder, int adapterPosition) { 23 | 24 | } 25 | 26 | @Override 27 | public void onScrollEnd(@NonNull T currentItemHolder, int adapterPosition) { 28 | 29 | } 30 | 31 | @Override 32 | public void onScroll(float scrollPosition, 33 | int currentIndex, int newIndex, 34 | @Nullable T currentHolder, @Nullable T newCurrentHolder) { 35 | adaptee.onScroll(scrollPosition, currentIndex, newIndex, currentHolder, newCurrentHolder); 36 | } 37 | 38 | @Override 39 | public boolean equals(Object obj) { 40 | if (obj instanceof ScrollListenerAdapter) { 41 | return adaptee.equals(((ScrollListenerAdapter) obj).adaptee); 42 | } else { 43 | return super.equals(obj); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/res/values/attr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | You should not set LayoutManager on DiscreteScrollView.class instance. Library uses a special one. Just don\'t call the method. 3 | InfiniteScrollAdapter is supposed to work only with DiscreteScrollView 4 | 5 | -------------------------------------------------------------------------------- /library/src/test/java/com/yarolegovich/discretescrollview/HorizontalDiscreteScrollLayoutManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.robolectric.RobolectricTestRunner; 5 | import org.robolectric.annotation.Config; 6 | 7 | /** 8 | * Created by yarolegovich on 10/28/17. 9 | */ 10 | @RunWith(RobolectricTestRunner.class) 11 | @Config(manifest = Config.NONE) 12 | public class HorizontalDiscreteScrollLayoutManagerTest extends DiscreteScrollLayoutManagerTest { 13 | 14 | @Override 15 | protected DSVOrientation getOrientationToTest() { 16 | return DSVOrientation.HORIZONTAL; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /library/src/test/java/com/yarolegovich/discretescrollview/VerticalDiscreteScrollLayoutManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.robolectric.RobolectricTestRunner; 5 | import org.robolectric.annotation.Config; 6 | 7 | /** 8 | * Created by yarolegovich on 10/28/17. 9 | */ 10 | @RunWith(RobolectricTestRunner.class) 11 | @Config(manifest = Config.NONE) 12 | public class VerticalDiscreteScrollLayoutManagerTest extends DiscreteScrollLayoutManagerTest { 13 | 14 | @Override 15 | protected DSVOrientation getOrientationToTest() { 16 | return DSVOrientation.VERTICAL; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /library/src/test/java/com/yarolegovich/discretescrollview/stub/StubRecyclerViewProxy.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.stub; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.recyclerview.widget.RecyclerView; 7 | 8 | import com.yarolegovich.discretescrollview.RecyclerViewProxy; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import static org.mockito.Mockito.mock; 14 | 15 | /** 16 | * Created by yarolegovich on 10/28/17. 17 | */ 18 | 19 | public class StubRecyclerViewProxy extends RecyclerViewProxy { 20 | 21 | private int width, height; 22 | private int childWidth, childHeight; 23 | private List children; 24 | private int adapterItemCount; 25 | 26 | public StubRecyclerViewProxy(@NonNull RecyclerView.LayoutManager layoutManager) { 27 | super(layoutManager); 28 | children = new ArrayList<>(); 29 | } 30 | 31 | @Override 32 | public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) { 33 | for (StubChildInfo childInfo : children) { 34 | recycleView(childInfo.view, recycler); 35 | } 36 | removeAllViews(); 37 | } 38 | 39 | @Override 40 | public int getChildCount() { 41 | return children.size(); 42 | } 43 | 44 | @Override 45 | public int getItemCount() { 46 | return adapterItemCount; 47 | } 48 | 49 | @Override 50 | public View getMeasuredChildForAdapterPosition(int position, RecyclerView.Recycler recycler) { 51 | if (position < adapterItemCount) { 52 | return new StubChildInfo(0, position).view; 53 | } 54 | throw new IndexOutOfBoundsException(); 55 | } 56 | 57 | @Override 58 | public View getChildAt(int index) { 59 | return children.get(index).view; 60 | } 61 | 62 | @Override 63 | public int getPosition(View view) { 64 | for (StubChildInfo info : children) { 65 | if (info.view == view) return info.adapterPosition; 66 | } 67 | throw new IllegalArgumentException(); 68 | } 69 | 70 | @Override 71 | public int getMeasuredWidthWithMargin(View child) { 72 | return childWidth; 73 | } 74 | 75 | @Override 76 | public int getMeasuredHeightWithMargin(View child) { 77 | return childHeight; 78 | } 79 | 80 | @Override 81 | public int getWidth() { 82 | return width; 83 | } 84 | 85 | @Override 86 | public int getHeight() { 87 | return height; 88 | } 89 | 90 | @Override 91 | public void removeAllViews() { 92 | children.clear(); 93 | } 94 | 95 | @Override 96 | public void offsetChildrenHorizontal(int amount) { 97 | //NOP 98 | } 99 | 100 | @Override 101 | public void offsetChildrenVertical(int amount) { 102 | //NOP 103 | } 104 | 105 | @Override 106 | public void attachView(View view) { 107 | //NOP 108 | } 109 | 110 | @Override 111 | public void detachView(View view) { 112 | //NOP 113 | } 114 | 115 | @Override 116 | public void detachAndScrapView(View view, RecyclerView.Recycler recycler) { 117 | //NOP 118 | } 119 | 120 | @Override 121 | public void detachAndScrapAttachedViews(RecyclerView.Recycler recycler) { 122 | //NOP 123 | } 124 | 125 | @Override 126 | public void recycleView(View view, RecyclerView.Recycler recycler) { 127 | //NOP 128 | } 129 | 130 | @Override 131 | public void layoutDecoratedWithMargins(View v, int left, int top, int right, int bottom) { 132 | //NOP 133 | } 134 | 135 | @Override 136 | public void requestLayout() { 137 | //NOP 138 | } 139 | 140 | @Override 141 | public void startSmoothScroll(RecyclerView.SmoothScroller smoothScroller) { 142 | //NOP 143 | } 144 | 145 | public void addChildren(int childCount, int firstChildAdapterPosition) { 146 | for (int i = 0; i < childCount; i++) { 147 | children.add(new StubChildInfo(i, firstChildAdapterPosition + i)); 148 | } 149 | } 150 | 151 | public void setAdapterItemCount(int adapterItemCount) { 152 | this.adapterItemCount = adapterItemCount; 153 | } 154 | 155 | private static class StubChildInfo { 156 | public final View view; 157 | public final int recyclerChildIndex; 158 | public final int adapterPosition; 159 | 160 | private StubChildInfo(int recyclerChildIndex, int adapterPosition) { 161 | this.view = mock(View.class); 162 | this.recyclerChildIndex = recyclerChildIndex; 163 | this.adapterPosition = adapterPosition; 164 | } 165 | } 166 | 167 | public static class Builder { 168 | StubRecyclerViewProxy target; 169 | 170 | public Builder(RecyclerView.LayoutManager lm) { 171 | target = new StubRecyclerViewProxy(lm); 172 | } 173 | 174 | public Builder withAdapterItemCount(int count) { 175 | target.adapterItemCount = count; 176 | return this; 177 | } 178 | 179 | public Builder withRecyclerDimensions(int width, int height) { 180 | target.width = width; 181 | target.height = height; 182 | return this; 183 | } 184 | 185 | public Builder withChildDimensions(int width, int height) { 186 | target.childWidth = width; 187 | target.childHeight = height; 188 | return this; 189 | } 190 | 191 | public StubRecyclerViewProxy create() { 192 | return target; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /release-bintray.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'com.jfrog.bintray' 3 | 4 | def upload = [ 5 | user : 'yarolegovich', 6 | artifactId : 'discrete-scrollview', 7 | userOrg : 'yarolegovich', 8 | repository : 'maven', 9 | groupId : 'com.yarolegovich', 10 | uploadName : 'DiscreteScrollView', 11 | description: 'A scrollable list of items that centers the current element and provides easy-to-use APIs for cool item animations.', 12 | version : '1.5.1', 13 | licences : ['Apache-2.0'] 14 | ] 15 | 16 | task androidSourcesJar(type: Jar) { 17 | archiveClassifier.set('sources') 18 | from android.sourceSets.main.java.srcDirs 19 | } 20 | 21 | version upload.version 22 | 23 | afterEvaluate { 24 | 25 | publishing { 26 | publications { 27 | LibRelease(MavenPublication) { 28 | from components.release 29 | 30 | artifact androidSourcesJar 31 | 32 | artifactId upload.artifactId 33 | groupId upload.groupId 34 | version upload.version 35 | } 36 | } 37 | } 38 | 39 | Properties localProps = new Properties() 40 | localProps.load(project.rootProject.file('local.properties').newDataInputStream()) 41 | 42 | bintray { 43 | user = upload.user 44 | key = localProps.getProperty('bintray.api_key') 45 | publications = ['LibRelease'] 46 | configurations = ['archives'] 47 | pkg { 48 | name = upload.uploadName 49 | repo = upload.repository 50 | userOrg = upload.userOrg 51 | licenses = upload.licences 52 | publish = true 53 | dryRun = false 54 | version { 55 | name = upload.version 56 | desc = upload.description 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion rootProject.compileSdkVersion 5 | buildToolsVersion rootProject.buildToolsVersion 6 | 7 | defaultConfig { 8 | applicationId "com.yarolegovich.discretescrollview.sample" 9 | minSdkVersion 19 10 | targetSdkVersion rootProject.targetSdkVersion 11 | versionCode 4 12 | versionName "1.0" 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation deps.designSupport 25 | implementation deps.annotations 26 | implementation deps.glide 27 | implementation deps.materialPrefs 28 | 29 | implementation project(':library') 30 | } 31 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\yarolegovich\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample/sample-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/sample-release.apk -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 28 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/App.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample; 2 | 3 | import android.app.Application; 4 | 5 | /** 6 | * Created by yarolegovich on 08.03.2017. 7 | */ 8 | 9 | public class App extends Application { 10 | 11 | private static App instance; 12 | 13 | public static App getInstance() { 14 | return instance; 15 | } 16 | 17 | @Override 18 | public void onCreate() { 19 | super.onCreate(); 20 | instance = this; 21 | DiscreteScrollViewOptions.init(this); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/DiscreteScrollViewOptions.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample; 2 | 3 | import android.content.Context; 4 | import android.content.DialogInterface; 5 | import android.content.SharedPreferences; 6 | import android.preference.PreferenceManager; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.view.View; 10 | 11 | import androidx.appcompat.widget.PopupMenu; 12 | import androidx.recyclerview.widget.RecyclerView; 13 | 14 | import com.google.android.material.bottomsheet.BottomSheetDialog; 15 | import com.yarolegovich.discretescrollview.DiscreteScrollView; 16 | import com.yarolegovich.discretescrollview.InfiniteScrollAdapter; 17 | 18 | import java.lang.ref.WeakReference; 19 | 20 | /** 21 | * Created by yarolegovich on 08.03.2017. 22 | */ 23 | 24 | public class DiscreteScrollViewOptions { 25 | 26 | private static DiscreteScrollViewOptions instance; 27 | 28 | private final String KEY_TRANSITION_TIME; 29 | 30 | public static void init(Context context) { 31 | instance = new DiscreteScrollViewOptions(context); 32 | } 33 | 34 | private DiscreteScrollViewOptions(Context context) { 35 | KEY_TRANSITION_TIME = context.getString(R.string.pref_key_transition_time); 36 | } 37 | 38 | public static void configureTransitionTime(DiscreteScrollView scrollView) { 39 | final BottomSheetDialog bsd = new BottomSheetDialog(scrollView.getContext()); 40 | final TransitionTimeChangeListener timeChangeListener = new TransitionTimeChangeListener(scrollView); 41 | bsd.setContentView(R.layout.dialog_transition_time); 42 | defaultPrefs().registerOnSharedPreferenceChangeListener(timeChangeListener); 43 | bsd.setOnDismissListener(new DialogInterface.OnDismissListener() { 44 | @Override 45 | public void onDismiss(DialogInterface dialog) { 46 | defaultPrefs().unregisterOnSharedPreferenceChangeListener(timeChangeListener); 47 | } 48 | }); 49 | View dismissBtn = bsd.findViewById(R.id.dialog_btn_dismiss); 50 | if (dismissBtn != null) { 51 | dismissBtn.setOnClickListener(new View.OnClickListener() { 52 | @Override 53 | public void onClick(View v) { 54 | bsd.dismiss(); 55 | } 56 | }); 57 | } 58 | bsd.show(); 59 | } 60 | 61 | public static void smoothScrollToUserSelectedPosition(final DiscreteScrollView scrollView, View anchor) { 62 | PopupMenu popupMenu = new PopupMenu(scrollView.getContext(), anchor); 63 | Menu menu = popupMenu.getMenu(); 64 | final RecyclerView.Adapter adapter = scrollView.getAdapter(); 65 | int itemCount = (adapter instanceof InfiniteScrollAdapter) ? 66 | ((InfiniteScrollAdapter) adapter).getRealItemCount() : 67 | (adapter != null ? adapter.getItemCount() : 0); 68 | for (int i = 0; i < itemCount; i++) { 69 | menu.add(String.valueOf(i + 1)); 70 | } 71 | popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 72 | @Override 73 | public boolean onMenuItemClick(MenuItem item) { 74 | int destination = Integer.parseInt(String.valueOf(item.getTitle())) - 1; 75 | if (adapter instanceof InfiniteScrollAdapter) { 76 | destination = ((InfiniteScrollAdapter) adapter).getClosestPosition(destination); 77 | } 78 | scrollView.smoothScrollToPosition(destination); 79 | return true; 80 | } 81 | }); 82 | popupMenu.show(); 83 | } 84 | 85 | public static int getTransitionTime() { 86 | return defaultPrefs().getInt(instance.KEY_TRANSITION_TIME, 150); 87 | } 88 | 89 | @SuppressWarnings("deprecation") 90 | private static SharedPreferences defaultPrefs() { 91 | return PreferenceManager.getDefaultSharedPreferences(App.getInstance()); 92 | } 93 | 94 | private static class TransitionTimeChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { 95 | 96 | private WeakReference scrollView; 97 | 98 | public TransitionTimeChangeListener(DiscreteScrollView scrollView) { 99 | this.scrollView = new WeakReference<>(scrollView); 100 | } 101 | 102 | @Override 103 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 104 | if (key.equals(instance.KEY_TRANSITION_TIME)) { 105 | DiscreteScrollView scrollView = this.scrollView.get(); 106 | if (scrollView != null) { 107 | scrollView.setItemTransitionTimeMillis(sharedPreferences.getInt(key, 150)); 108 | } else { 109 | sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.view.View; 10 | 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import androidx.appcompat.widget.Toolbar; 13 | 14 | import com.google.android.material.snackbar.Snackbar; 15 | import com.yarolegovich.discretescrollview.sample.gallery.GalleryActivity; 16 | import com.yarolegovich.discretescrollview.sample.shop.ShopActivity; 17 | import com.yarolegovich.discretescrollview.sample.weather.WeatherActivity; 18 | 19 | 20 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 21 | 22 | private static final Uri URL_TAYA_BEHANCE = Uri.parse("https://www.behance.net/yurkivt"); 23 | private static final Uri URL_SHOP_PHOTOS = Uri.parse("https://herriottgrace.com/collections/all"); 24 | private static final Uri URL_CITY_ICONS = Uri.parse("https://www.flaticon.com"); 25 | private static final Uri URL_APP_REPO = Uri.parse("https://github.com/yarolegovich/DiscreteScrollView"); 26 | 27 | private View root; 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | setContentView(R.layout.activity_main); 33 | 34 | root = findViewById(R.id.screen); 35 | 36 | Toolbar toolbar = findViewById(R.id.toolbar); 37 | setSupportActionBar(toolbar); 38 | 39 | findViewById(R.id.preview_shop).setOnClickListener(this); 40 | findViewById(R.id.preview_weather).setOnClickListener(this); 41 | findViewById(R.id.preview_vertical).setOnClickListener(this); 42 | 43 | findViewById(R.id.credit_city_icons).setOnClickListener(this); 44 | findViewById(R.id.credit_shop_photos).setOnClickListener(this); 45 | findViewById(R.id.credit_taya).setOnClickListener(this); 46 | } 47 | 48 | @Override 49 | public boolean onCreateOptionsMenu(Menu menu) { 50 | getMenuInflater().inflate(R.menu.main, menu); 51 | return super.onCreateOptionsMenu(menu); 52 | } 53 | 54 | @Override 55 | public boolean onOptionsItemSelected(MenuItem item) { 56 | if (item.getItemId() == R.id.mi_github) { 57 | open(URL_APP_REPO); 58 | return true; 59 | } 60 | return super.onOptionsItemSelected(item); 61 | } 62 | 63 | @Override 64 | public void onClick(View v) { 65 | switch (v.getId()) { 66 | case R.id.preview_shop: 67 | start(ShopActivity.class); 68 | break; 69 | case R.id.preview_weather: 70 | start(WeatherActivity.class); 71 | break; 72 | case R.id.preview_vertical: 73 | start(GalleryActivity.class); 74 | break; 75 | case R.id.credit_city_icons: 76 | open(URL_CITY_ICONS); 77 | break; 78 | case R.id.credit_shop_photos: 79 | open(URL_SHOP_PHOTOS); 80 | break; 81 | case R.id.credit_taya: 82 | open(URL_TAYA_BEHANCE); 83 | break; 84 | } 85 | } 86 | 87 | private void open(Uri url) { 88 | Intent intent = new Intent(Intent.ACTION_VIEW); 89 | intent.setData(url); 90 | if (intent.resolveActivity(getPackageManager()) != null) { 91 | startActivity(intent); 92 | } else { 93 | Snackbar.make(root, 94 | R.string.msg_no_browser, 95 | Snackbar.LENGTH_SHORT) 96 | .show(); 97 | } 98 | } 99 | 100 | private void start(Class token) { 101 | Intent intent = new Intent(this, token); 102 | startActivity(intent); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/Gallery.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.gallery; 2 | 3 | import com.yarolegovich.discretescrollview.sample.R; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | /** 9 | * Created by yarolegovich on 16.03.2017. 10 | */ 11 | 12 | public class Gallery { 13 | 14 | public static Gallery get() { 15 | return new Gallery(); 16 | } 17 | 18 | private Gallery() { 19 | } 20 | 21 | public List getData() { 22 | return Arrays.asList( 23 | new Image(R.drawable.shop1), 24 | new Image(R.drawable.shop2), 25 | new Image(R.drawable.shop3), 26 | new Image(R.drawable.shop4), 27 | new Image(R.drawable.shop5), 28 | new Image(R.drawable.shop6)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryActivity.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.gallery; 2 | 3 | import android.animation.ArgbEvaluator; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import androidx.annotation.Nullable; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import androidx.core.content.ContextCompat; 10 | 11 | import com.google.android.material.snackbar.Snackbar; 12 | import com.yarolegovich.discretescrollview.DiscreteScrollView; 13 | import com.yarolegovich.discretescrollview.sample.R; 14 | 15 | import java.util.List; 16 | 17 | public class GalleryActivity extends AppCompatActivity implements 18 | DiscreteScrollView.ScrollListener, 19 | DiscreteScrollView.OnItemChangedListener, 20 | View.OnClickListener { 21 | 22 | private ArgbEvaluator evaluator; 23 | private int currentOverlayColor; 24 | private int overlayColor; 25 | 26 | @Override 27 | protected void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | setContentView(R.layout.activity_gallery); 30 | 31 | evaluator = new ArgbEvaluator(); 32 | currentOverlayColor = ContextCompat.getColor(this, R.color.galleryCurrentItemOverlay); 33 | overlayColor = ContextCompat.getColor(this, R.color.galleryItemOverlay); 34 | 35 | Gallery gallery = Gallery.get(); 36 | List data = gallery.getData(); 37 | DiscreteScrollView itemPicker = findViewById(R.id.item_picker); 38 | itemPicker.setAdapter(new GalleryAdapter(data)); 39 | itemPicker.addScrollListener(this); 40 | itemPicker.addOnItemChangedListener(this); 41 | itemPicker.scrollToPosition(1); 42 | 43 | findViewById(R.id.home).setOnClickListener(this); 44 | findViewById(R.id.fab_share).setOnClickListener(this); 45 | } 46 | 47 | @Override 48 | public void onClick(View v) { 49 | switch (v.getId()) { 50 | case R.id.home: 51 | finish(); 52 | break; 53 | case R.id.fab_share: 54 | share(v); 55 | break; 56 | } 57 | } 58 | 59 | @Override 60 | public void onScroll( 61 | float currentPosition, 62 | int currentIndex, int newIndex, 63 | @Nullable GalleryAdapter.ViewHolder currentHolder, 64 | @Nullable GalleryAdapter.ViewHolder newCurrent) { 65 | if (currentHolder != null && newCurrent != null) { 66 | float position = Math.abs(currentPosition); 67 | currentHolder.setOverlayColor(interpolate(position, currentOverlayColor, overlayColor)); 68 | newCurrent.setOverlayColor(interpolate(position, overlayColor, currentOverlayColor)); 69 | } 70 | } 71 | 72 | @Override 73 | public void onCurrentItemChanged(@Nullable GalleryAdapter.ViewHolder viewHolder, int adapterPosition) { 74 | //viewHolder will never be null, because we never remove items from adapter's list 75 | if (viewHolder != null) { 76 | viewHolder.setOverlayColor(currentOverlayColor); 77 | } 78 | } 79 | 80 | private void share(View view) { 81 | Snackbar.make(view, R.string.msg_unsupported_op, Snackbar.LENGTH_SHORT).show(); 82 | } 83 | 84 | private int interpolate(float fraction, int c1, int c2) { 85 | return (int) evaluator.evaluate(fraction, c1, c2); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/GalleryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.gallery; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Point; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | 10 | import androidx.annotation.ColorInt; 11 | import androidx.annotation.NonNull; 12 | import androidx.recyclerview.widget.RecyclerView; 13 | 14 | import com.bumptech.glide.Glide; 15 | import com.yarolegovich.discretescrollview.sample.R; 16 | 17 | import java.util.List; 18 | 19 | /** 20 | * Created by yarolegovich on 16.03.2017. 21 | */ 22 | 23 | public class GalleryAdapter extends RecyclerView.Adapter { 24 | 25 | private int itemHeight; 26 | private List data; 27 | 28 | public GalleryAdapter(List data) { 29 | this.data = data; 30 | } 31 | 32 | @Override 33 | public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { 34 | super.onAttachedToRecyclerView(recyclerView); 35 | Activity context = (Activity) recyclerView.getContext(); 36 | Point windowDimensions = new Point(); 37 | context.getWindowManager().getDefaultDisplay().getSize(windowDimensions); 38 | itemHeight = Math.round(windowDimensions.y * 0.6f); 39 | } 40 | 41 | @NonNull 42 | @Override 43 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 44 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 45 | View v = inflater.inflate(R.layout.item_gallery, parent, false); 46 | ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( 47 | ViewGroup.LayoutParams.MATCH_PARENT, 48 | itemHeight); 49 | v.setLayoutParams(params); 50 | return new ViewHolder(v); 51 | } 52 | 53 | @Override 54 | public void onBindViewHolder(ViewHolder holder, int position) { 55 | Glide.with(holder.itemView.getContext()) 56 | .load(data.get(position).getResource()) 57 | .into(holder.image); 58 | } 59 | 60 | @Override 61 | public int getItemCount() { 62 | return data.size(); 63 | } 64 | 65 | static class ViewHolder extends RecyclerView.ViewHolder { 66 | 67 | private View overlay; 68 | private ImageView image; 69 | 70 | public ViewHolder(View itemView) { 71 | super(itemView); 72 | image = itemView.findViewById(R.id.image); 73 | overlay = itemView.findViewById(R.id.overlay); 74 | } 75 | 76 | public void setOverlayColor(@ColorInt int color) { 77 | overlay.setBackgroundColor(color); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/gallery/Image.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.gallery; 2 | 3 | /** 4 | * Created by yarolegovich on 16.03.2017. 5 | */ 6 | 7 | public class Image { 8 | 9 | private final int res; 10 | 11 | public Image(int res) { 12 | this.res = res; 13 | } 14 | 15 | public int getResource() { 16 | return res; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/Item.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.shop; 2 | 3 | /** 4 | * Created by yarolegovich on 07.03.2017. 5 | */ 6 | 7 | public class Item { 8 | 9 | private final int id; 10 | private final String name; 11 | private final String price; 12 | private final int image; 13 | 14 | public Item(int id, String name, String price, int image) { 15 | this.id = id; 16 | this.name = name; 17 | this.price = price; 18 | this.image = image; 19 | } 20 | 21 | public int getId() { 22 | return id; 23 | } 24 | 25 | public String getName() { 26 | return name; 27 | } 28 | 29 | public String getPrice() { 30 | return price; 31 | } 32 | 33 | public int getImage() { 34 | return image; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/Shop.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.shop; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.yarolegovich.discretescrollview.sample.App; 7 | import com.yarolegovich.discretescrollview.sample.R; 8 | 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | /** 13 | * Created by yarolegovich on 07.03.2017. 14 | */ 15 | 16 | public class Shop { 17 | 18 | private static final String STORAGE = "shop"; 19 | 20 | public static Shop get() { 21 | return new Shop(); 22 | } 23 | 24 | private SharedPreferences storage; 25 | 26 | private Shop() { 27 | storage = App.getInstance().getSharedPreferences(STORAGE, Context.MODE_PRIVATE); 28 | } 29 | 30 | public List getData() { 31 | return Arrays.asList( 32 | new Item(1, "Everyday Candle", "$12.00 USD", R.drawable.shop1), 33 | new Item(2, "Small Porcelain Bowl", "$50.00 USD", R.drawable.shop2), 34 | new Item(3, "Favourite Board", "$265.00 USD", R.drawable.shop3), 35 | new Item(4, "Earthenware Bowl", "$18.00 USD", R.drawable.shop4), 36 | new Item(5, "Porcelain Dessert Plate", "$36.00 USD", R.drawable.shop5), 37 | new Item(6, "Detailed Rolling Pin", "$145.00 USD", R.drawable.shop6)); 38 | } 39 | 40 | public boolean isRated(int itemId) { 41 | return storage.getBoolean(String.valueOf(itemId), false); 42 | } 43 | 44 | public void setRated(int itemId, boolean isRated) { 45 | storage.edit().putBoolean(String.valueOf(itemId), isRated).apply(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopActivity.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.shop; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.ImageView; 6 | import android.widget.TextView; 7 | 8 | import androidx.annotation.Nullable; 9 | import androidx.appcompat.app.AppCompatActivity; 10 | import androidx.core.content.ContextCompat; 11 | 12 | import com.google.android.material.snackbar.Snackbar; 13 | import com.yarolegovich.discretescrollview.DSVOrientation; 14 | import com.yarolegovich.discretescrollview.DiscreteScrollView; 15 | import com.yarolegovich.discretescrollview.InfiniteScrollAdapter; 16 | import com.yarolegovich.discretescrollview.sample.DiscreteScrollViewOptions; 17 | import com.yarolegovich.discretescrollview.sample.R; 18 | import com.yarolegovich.discretescrollview.transform.ScaleTransformer; 19 | 20 | import java.util.List; 21 | 22 | /** 23 | * Created by yarolegovich on 07.03.2017. 24 | */ 25 | 26 | public class ShopActivity extends AppCompatActivity implements DiscreteScrollView.OnItemChangedListener, 27 | View.OnClickListener { 28 | 29 | private List data; 30 | private Shop shop; 31 | 32 | private TextView currentItemName; 33 | private TextView currentItemPrice; 34 | private ImageView rateItemButton; 35 | private DiscreteScrollView itemPicker; 36 | private InfiniteScrollAdapter infiniteAdapter; 37 | 38 | @Override 39 | protected void onCreate(@Nullable Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_shop); 42 | 43 | currentItemName = findViewById(R.id.item_name); 44 | currentItemPrice = findViewById(R.id.item_price); 45 | rateItemButton = findViewById(R.id.item_btn_rate); 46 | 47 | shop = Shop.get(); 48 | data = shop.getData(); 49 | itemPicker = findViewById(R.id.item_picker); 50 | itemPicker.setOrientation(DSVOrientation.HORIZONTAL); 51 | itemPicker.addOnItemChangedListener(this); 52 | infiniteAdapter = InfiniteScrollAdapter.wrap(new ShopAdapter(data)); 53 | itemPicker.setAdapter(infiniteAdapter); 54 | itemPicker.setItemTransitionTimeMillis(DiscreteScrollViewOptions.getTransitionTime()); 55 | itemPicker.setItemTransformer(new ScaleTransformer.Builder() 56 | .setMinScale(0.8f) 57 | .build()); 58 | 59 | onItemChanged(data.get(0)); 60 | 61 | findViewById(R.id.item_btn_rate).setOnClickListener(this); 62 | findViewById(R.id.item_btn_buy).setOnClickListener(this); 63 | findViewById(R.id.item_btn_comment).setOnClickListener(this); 64 | 65 | findViewById(R.id.home).setOnClickListener(this); 66 | findViewById(R.id.btn_smooth_scroll).setOnClickListener(this); 67 | findViewById(R.id.btn_transition_time).setOnClickListener(this); 68 | } 69 | 70 | @Override 71 | public void onClick(View v) { 72 | switch (v.getId()) { 73 | case R.id.item_btn_rate: 74 | int realPosition = infiniteAdapter.getRealPosition(itemPicker.getCurrentItem()); 75 | Item current = data.get(realPosition); 76 | shop.setRated(current.getId(), !shop.isRated(current.getId())); 77 | changeRateButtonState(current); 78 | break; 79 | case R.id.home: 80 | finish(); 81 | break; 82 | case R.id.btn_transition_time: 83 | DiscreteScrollViewOptions.configureTransitionTime(itemPicker); 84 | break; 85 | case R.id.btn_smooth_scroll: 86 | DiscreteScrollViewOptions.smoothScrollToUserSelectedPosition(itemPicker, v); 87 | break; 88 | default: 89 | showUnsupportedSnackBar(); 90 | break; 91 | } 92 | } 93 | 94 | private void onItemChanged(Item item) { 95 | currentItemName.setText(item.getName()); 96 | currentItemPrice.setText(item.getPrice()); 97 | changeRateButtonState(item); 98 | } 99 | 100 | private void changeRateButtonState(Item item) { 101 | if (shop.isRated(item.getId())) { 102 | rateItemButton.setImageResource(R.drawable.ic_star_black_24dp); 103 | rateItemButton.setColorFilter(ContextCompat.getColor(this, R.color.shopRatedStar)); 104 | } else { 105 | rateItemButton.setImageResource(R.drawable.ic_star_border_black_24dp); 106 | rateItemButton.setColorFilter(ContextCompat.getColor(this, R.color.shopSecondary)); 107 | } 108 | } 109 | 110 | @Override 111 | public void onCurrentItemChanged(@Nullable ShopAdapter.ViewHolder viewHolder, int adapterPosition) { 112 | int positionInDataSet = infiniteAdapter.getRealPosition(adapterPosition); 113 | onItemChanged(data.get(positionInDataSet)); 114 | } 115 | 116 | private void showUnsupportedSnackBar() { 117 | Snackbar.make(itemPicker, R.string.msg_unsupported_op, Snackbar.LENGTH_SHORT).show(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/shop/ShopAdapter.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.shop; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.ImageView; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.recyclerview.widget.RecyclerView; 10 | 11 | import com.bumptech.glide.Glide; 12 | import com.yarolegovich.discretescrollview.sample.R; 13 | 14 | import java.util.List; 15 | 16 | /** 17 | * Created by yarolegovich on 07.03.2017. 18 | */ 19 | 20 | public class ShopAdapter extends RecyclerView.Adapter { 21 | 22 | private List data; 23 | 24 | public ShopAdapter(List data) { 25 | this.data = data; 26 | } 27 | 28 | @NonNull 29 | @Override 30 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 31 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 32 | View v = inflater.inflate(R.layout.item_shop_card, parent, false); 33 | return new ViewHolder(v); 34 | } 35 | 36 | @Override 37 | public void onBindViewHolder(ViewHolder holder, int position) { 38 | Glide.with(holder.itemView.getContext()) 39 | .load(data.get(position).getImage()) 40 | .into(holder.image); 41 | } 42 | 43 | @Override 44 | public int getItemCount() { 45 | return data.size(); 46 | } 47 | 48 | static class ViewHolder extends RecyclerView.ViewHolder { 49 | 50 | private ImageView image; 51 | 52 | public ViewHolder(View itemView) { 53 | super(itemView); 54 | image = itemView.findViewById(R.id.image); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/Forecast.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.weather; 2 | 3 | /** 4 | * Created by yarolegovich on 08.03.2017. 5 | */ 6 | 7 | public class Forecast { 8 | 9 | private final String cityName; 10 | private final int cityIcon; 11 | private final String temperature; 12 | private final Weather weather; 13 | 14 | public Forecast(String cityName, int cityIcon, String temperature, Weather weather) { 15 | this.cityName = cityName; 16 | this.cityIcon = cityIcon; 17 | this.temperature = temperature; 18 | this.weather = weather; 19 | } 20 | 21 | public String getCityName() { 22 | return cityName; 23 | } 24 | 25 | public int getCityIcon() { 26 | return cityIcon; 27 | } 28 | 29 | public String getTemperature() { 30 | return temperature; 31 | } 32 | 33 | public Weather getWeather() { 34 | return weather; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastAdapter.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.weather; 2 | 3 | import android.graphics.Color; 4 | import android.graphics.drawable.Drawable; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | import androidx.core.content.ContextCompat; 14 | import androidx.recyclerview.widget.RecyclerView; 15 | 16 | import com.bumptech.glide.Glide; 17 | import com.bumptech.glide.load.DataSource; 18 | import com.bumptech.glide.load.engine.GlideException; 19 | import com.bumptech.glide.request.RequestListener; 20 | import com.bumptech.glide.request.target.Target; 21 | import com.yarolegovich.discretescrollview.sample.R; 22 | 23 | import java.util.List; 24 | 25 | /** 26 | * Created by yarolegovich on 08.03.2017. 27 | */ 28 | 29 | public class ForecastAdapter extends RecyclerView.Adapter { 30 | 31 | private RecyclerView parentRecycler; 32 | private List data; 33 | 34 | public ForecastAdapter(List data) { 35 | this.data = data; 36 | } 37 | 38 | @Override 39 | public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { 40 | super.onAttachedToRecyclerView(recyclerView); 41 | parentRecycler = recyclerView; 42 | } 43 | 44 | @NonNull 45 | @Override 46 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 47 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 48 | View v = inflater.inflate(R.layout.item_city_card, parent, false); 49 | return new ViewHolder(v); 50 | } 51 | 52 | @Override 53 | public void onBindViewHolder(ViewHolder holder, int position) { 54 | int iconTint = ContextCompat.getColor(holder.itemView.getContext(), R.color.grayIconTint); 55 | Forecast forecast = data.get(position); 56 | Glide.with(holder.itemView.getContext()) 57 | .load(forecast.getCityIcon()) 58 | .listener(new TintOnLoad(holder.imageView, iconTint)) 59 | .into(holder.imageView); 60 | holder.textView.setText(forecast.getCityName()); 61 | } 62 | 63 | @Override 64 | public int getItemCount() { 65 | return data.size(); 66 | } 67 | 68 | class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 69 | 70 | private ImageView imageView; 71 | private TextView textView; 72 | 73 | public ViewHolder(View itemView) { 74 | super(itemView); 75 | imageView = itemView.findViewById(R.id.city_image); 76 | textView = itemView.findViewById(R.id.city_name); 77 | 78 | itemView.findViewById(R.id.container).setOnClickListener(this); 79 | } 80 | 81 | public void showText() { 82 | int parentHeight = ((View) imageView.getParent()).getHeight(); 83 | float scale = (parentHeight - textView.getHeight()) / (float) imageView.getHeight(); 84 | imageView.setPivotX(imageView.getWidth() * 0.5f); 85 | imageView.setPivotY(0); 86 | imageView.animate().scaleX(scale) 87 | .withEndAction(new Runnable() { 88 | @Override 89 | public void run() { 90 | textView.setVisibility(View.VISIBLE); 91 | imageView.setColorFilter(Color.BLACK); 92 | } 93 | }) 94 | .scaleY(scale).setDuration(200) 95 | .start(); 96 | } 97 | 98 | public void hideText() { 99 | imageView.setColorFilter(ContextCompat.getColor(imageView.getContext(), R.color.grayIconTint)); 100 | textView.setVisibility(View.INVISIBLE); 101 | imageView.animate().scaleX(1f).scaleY(1f) 102 | .setDuration(200) 103 | .start(); 104 | } 105 | 106 | @Override 107 | public void onClick(View v) { 108 | parentRecycler.smoothScrollToPosition(getAdapterPosition()); 109 | } 110 | } 111 | 112 | private static class TintOnLoad implements RequestListener { 113 | 114 | private ImageView imageView; 115 | private int tintColor; 116 | 117 | public TintOnLoad(ImageView view, int tintColor) { 118 | this.imageView = view; 119 | this.tintColor = tintColor; 120 | } 121 | 122 | @Override 123 | public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { 124 | imageView.setColorFilter(tintColor); 125 | return false; 126 | } 127 | 128 | @Override 129 | public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { 130 | return false; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/ForecastView.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.weather; 2 | 3 | import android.animation.ArgbEvaluator; 4 | import android.content.Context; 5 | import android.graphics.Canvas; 6 | import android.graphics.LinearGradient; 7 | import android.graphics.Paint; 8 | import android.graphics.Shader; 9 | import android.util.AttributeSet; 10 | import android.view.Gravity; 11 | import android.view.animation.AccelerateDecelerateInterpolator; 12 | import android.widget.ImageView; 13 | import android.widget.LinearLayout; 14 | import android.widget.TextView; 15 | 16 | import androidx.annotation.ArrayRes; 17 | 18 | import com.bumptech.glide.Glide; 19 | import com.yarolegovich.discretescrollview.sample.R; 20 | 21 | /** 22 | * Created by yarolegovich on 08.03.2017. 23 | */ 24 | 25 | public class ForecastView extends LinearLayout { 26 | 27 | private Paint gradientPaint; 28 | private int[] currentGradient; 29 | 30 | private TextView weatherDescription; 31 | private TextView weatherTemperature; 32 | private ImageView weatherImage; 33 | 34 | private ArgbEvaluator evaluator; 35 | 36 | public ForecastView(Context context) { 37 | super(context); 38 | } 39 | 40 | public ForecastView(Context context, AttributeSet attrs) { 41 | super(context, attrs); 42 | } 43 | 44 | public ForecastView(Context context, AttributeSet attrs, int defStyleAttr) { 45 | super(context, attrs, defStyleAttr); 46 | } 47 | 48 | { 49 | evaluator = new ArgbEvaluator(); 50 | 51 | gradientPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 52 | setWillNotDraw(false); 53 | 54 | setOrientation(VERTICAL); 55 | setGravity(Gravity.CENTER_HORIZONTAL); 56 | inflate(getContext(), R.layout.view_forecast, this); 57 | 58 | weatherDescription = findViewById(R.id.weather_description); 59 | weatherImage = findViewById(R.id.weather_image); 60 | weatherTemperature = findViewById(R.id.weather_temperature); 61 | } 62 | 63 | private void initGradient() { 64 | float centerX = getWidth() * 0.5f; 65 | Shader gradient = new LinearGradient( 66 | centerX, 0, centerX, getHeight(), 67 | currentGradient, null, 68 | Shader.TileMode.MIRROR); 69 | gradientPaint.setShader(gradient); 70 | } 71 | 72 | @Override 73 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 74 | super.onSizeChanged(w, h, oldw, oldh); 75 | if (currentGradient != null) { 76 | initGradient(); 77 | } 78 | } 79 | 80 | @Override 81 | protected void onDraw(Canvas canvas) { 82 | canvas.drawRect(0, 0, getWidth(), getHeight(), gradientPaint); 83 | super.onDraw(canvas); 84 | } 85 | 86 | public void setForecast(Forecast forecast) { 87 | Weather weather = forecast.getWeather(); 88 | currentGradient = weatherToGradient(weather); 89 | if (getWidth() != 0 && getHeight() != 0) { 90 | initGradient(); 91 | } 92 | weatherDescription.setText(weather.getDisplayName()); 93 | weatherTemperature.setText(forecast.getTemperature()); 94 | Glide.with(getContext()).load(weatherToIcon(weather)).into(weatherImage); 95 | invalidate(); 96 | 97 | weatherImage.animate() 98 | .scaleX(1f).scaleY(1f) 99 | .setInterpolator(new AccelerateDecelerateInterpolator()) 100 | .setDuration(300) 101 | .start(); 102 | } 103 | 104 | public void onScroll(float fraction, Forecast oldF, Forecast newF) { 105 | weatherImage.setScaleX(fraction); 106 | weatherImage.setScaleY(fraction); 107 | currentGradient = mix(fraction, 108 | weatherToGradient(newF.getWeather()), 109 | weatherToGradient(oldF.getWeather())); 110 | initGradient(); 111 | invalidate(); 112 | } 113 | 114 | private int[] mix(float fraction, int[] c1, int[] c2) { 115 | return new int[]{ 116 | (Integer) evaluator.evaluate(fraction, c1[0], c2[0]), 117 | (Integer) evaluator.evaluate(fraction, c1[1], c2[1]), 118 | (Integer) evaluator.evaluate(fraction, c1[2], c2[2]) 119 | }; 120 | } 121 | 122 | private int[] weatherToGradient(Weather weather) { 123 | switch (weather) { 124 | case PERIODIC_CLOUDS: 125 | return colors(R.array.gradientPeriodicClouds); 126 | case CLOUDY: 127 | return colors(R.array.gradientCloudy); 128 | case MOSTLY_CLOUDY: 129 | return colors(R.array.gradientMostlyCloudy); 130 | case PARTLY_CLOUDY: 131 | return colors(R.array.gradientPartlyCloudy); 132 | case CLEAR: 133 | return colors(R.array.gradientClear); 134 | default: 135 | throw new IllegalArgumentException(); 136 | } 137 | } 138 | 139 | private int weatherToIcon(Weather weather) { 140 | switch (weather) { 141 | case PERIODIC_CLOUDS: 142 | return R.drawable.periodic_clouds; 143 | case CLOUDY: 144 | return R.drawable.cloudy; 145 | case MOSTLY_CLOUDY: 146 | return R.drawable.mostly_cloudy; 147 | case PARTLY_CLOUDY: 148 | return R.drawable.partly_cloudy; 149 | case CLEAR: 150 | return R.drawable.clear; 151 | default: 152 | throw new IllegalArgumentException(); 153 | } 154 | } 155 | 156 | private int[] colors(@ArrayRes int res) { 157 | return getContext().getResources().getIntArray(res); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/Weather.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.weather; 2 | 3 | /** 4 | * Created by yarolegovich on 08.03.2017. 5 | */ 6 | 7 | public enum Weather { 8 | 9 | PERIODIC_CLOUDS("Periodic Clouds"), 10 | CLOUDY("Cloudy"), 11 | MOSTLY_CLOUDY("Mostly Cloudy"), 12 | PARTLY_CLOUDY("Partly Cloudy"), 13 | CLEAR("Clear"); 14 | 15 | private String displayName; 16 | 17 | Weather(String displayName) { 18 | this.displayName = displayName; 19 | } 20 | 21 | public String getDisplayName() { 22 | return displayName; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherActivity.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.weather; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import androidx.recyclerview.widget.RecyclerView; 10 | 11 | import com.yarolegovich.discretescrollview.DiscreteScrollView; 12 | import com.yarolegovich.discretescrollview.sample.R; 13 | import com.yarolegovich.discretescrollview.sample.DiscreteScrollViewOptions; 14 | import com.yarolegovich.discretescrollview.transform.ScaleTransformer; 15 | 16 | import java.util.List; 17 | 18 | /** 19 | * Created by yarolegovich on 08.03.2017. 20 | */ 21 | 22 | public class WeatherActivity extends AppCompatActivity implements 23 | DiscreteScrollView.ScrollStateChangeListener, 24 | DiscreteScrollView.OnItemChangedListener, 25 | View.OnClickListener { 26 | 27 | private List forecasts; 28 | 29 | private ForecastView forecastView; 30 | private DiscreteScrollView cityPicker; 31 | 32 | @Override 33 | protected void onCreate(@Nullable Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_weather); 36 | 37 | forecastView = findViewById(R.id.forecast_view); 38 | 39 | forecasts = WeatherStation.get().getForecasts(); 40 | cityPicker = findViewById(R.id.forecast_city_picker); 41 | cityPicker.setSlideOnFling(true); 42 | cityPicker.setAdapter(new ForecastAdapter(forecasts)); 43 | cityPicker.addOnItemChangedListener(this); 44 | cityPicker.addScrollStateChangeListener(this); 45 | cityPicker.scrollToPosition(2); 46 | cityPicker.setItemTransitionTimeMillis(DiscreteScrollViewOptions.getTransitionTime()); 47 | cityPicker.setItemTransformer(new ScaleTransformer.Builder() 48 | .setMinScale(0.8f) 49 | .build()); 50 | 51 | forecastView.setForecast(forecasts.get(0)); 52 | 53 | findViewById(R.id.home).setOnClickListener(this); 54 | findViewById(R.id.btn_transition_time).setOnClickListener(this); 55 | findViewById(R.id.btn_smooth_scroll).setOnClickListener(this); 56 | } 57 | 58 | @Override 59 | public void onCurrentItemChanged(@Nullable ForecastAdapter.ViewHolder holder, int position) { 60 | //viewHolder will never be null, because we never remove items from adapter's list 61 | if (holder != null) { 62 | forecastView.setForecast(forecasts.get(position)); 63 | holder.showText(); 64 | } 65 | } 66 | 67 | @Override 68 | public void onScrollStart(@NonNull ForecastAdapter.ViewHolder holder, int position) { 69 | holder.hideText(); 70 | } 71 | 72 | @Override 73 | public void onScroll( 74 | float position, 75 | int currentIndex, int newIndex, 76 | @Nullable ForecastAdapter.ViewHolder currentHolder, 77 | @Nullable ForecastAdapter.ViewHolder newHolder) { 78 | Forecast current = forecasts.get(currentIndex); 79 | RecyclerView.Adapter adapter = cityPicker.getAdapter(); 80 | int itemCount = adapter != null ? adapter.getItemCount() : 0; 81 | if (newIndex >= 0 && newIndex < itemCount) { 82 | Forecast next = forecasts.get(newIndex); 83 | forecastView.onScroll(1f - Math.abs(position), current, next); 84 | } 85 | } 86 | 87 | @Override 88 | public void onClick(View v) { 89 | switch (v.getId()) { 90 | case R.id.home: 91 | finish(); 92 | break; 93 | case R.id.btn_transition_time: 94 | DiscreteScrollViewOptions.configureTransitionTime(cityPicker); 95 | break; 96 | case R.id.btn_smooth_scroll: 97 | DiscreteScrollViewOptions.smoothScrollToUserSelectedPosition(cityPicker, v); 98 | break; 99 | } 100 | } 101 | 102 | @Override 103 | public void onScrollEnd(@NonNull ForecastAdapter.ViewHolder holder, int position) { 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yarolegovich/discretescrollview/sample/weather/WeatherStation.java: -------------------------------------------------------------------------------- 1 | package com.yarolegovich.discretescrollview.sample.weather; 2 | 3 | import com.yarolegovich.discretescrollview.sample.R; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | /** 9 | * Created by yarolegovich on 08.03.2017. 10 | */ 11 | 12 | public class WeatherStation { 13 | 14 | 15 | public static WeatherStation get() { 16 | return new WeatherStation(); 17 | } 18 | 19 | private WeatherStation() { 20 | } 21 | 22 | public List getForecasts() { 23 | return Arrays.asList( 24 | new Forecast("Pisa", R.drawable.pisa, "16", Weather.PARTLY_CLOUDY), 25 | new Forecast("Paris", R.drawable.paris, "14", Weather.CLEAR), 26 | new Forecast("New York", R.drawable.new_york, "9", Weather.MOSTLY_CLOUDY), 27 | new Forecast("Rome", R.drawable.rome, "18", Weather.PARTLY_CLOUDY), 28 | new Forecast("London", R.drawable.london, "6", Weather.PERIODIC_CLOUDS), 29 | new Forecast("Washington", R.drawable.washington, "20", Weather.CLEAR)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_arrow_back_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_arrow_back_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_behance_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_behance_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_collections_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_collections_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_comment_text_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_comment_text_outline_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_github_circle_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_github_circle_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_shopping_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_shopping_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_star_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_star_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_star_border_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_star_border_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_weather_partlycloudy_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-hdpi/ic_weather_partlycloudy_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_arrow_back_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_arrow_back_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_behance_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_behance_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_collections_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_collections_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_comment_text_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_comment_text_outline_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_github_circle_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_github_circle_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_shopping_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_shopping_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_star_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_star_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_star_border_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_star_border_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_weather_partlycloudy_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-mdpi/ic_weather_partlycloudy_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/london.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-nodpi/london.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/new_york.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-nodpi/new_york.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/paris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-nodpi/paris.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/pisa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-nodpi/pisa.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/rome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-nodpi/rome.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/washington.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-nodpi/washington.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_arrow_back_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_arrow_back_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_behance_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_behance_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_collections_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_collections_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_comment_text_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_comment_text_outline_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_github_circle_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_github_circle_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_shopping_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_shopping_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_star_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_star_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_star_border_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_star_border_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_weather_partlycloudy_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xhdpi/ic_weather_partlycloudy_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_arrow_back_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_arrow_back_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_behance_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_behance_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_collections_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_collections_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_comment_text_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_comment_text_outline_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_github_circle_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_github_circle_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_shopping_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_shopping_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_star_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_star_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_star_border_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_star_border_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_weather_partlycloudy_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxhdpi/ic_weather_partlycloudy_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_arrow_back_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_arrow_back_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_behance_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_behance_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_collections_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_collections_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_comment_text_outline_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_comment_text_outline_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_github_circle_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_github_circle_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_shopping_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_shopping_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_star_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_star_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_star_border_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_star_border_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_weather_partlycloudy_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable-xxxhdpi/ic_weather_partlycloudy_black_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/clear.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/cloudy.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/mostly_cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/mostly_cloudy.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/partly_cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/partly_cloudy.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/periodic_clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/periodic_clouds.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/shop1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/shop1.jpg -------------------------------------------------------------------------------- /sample/src/main/res/drawable/shop2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/shop2.jpg -------------------------------------------------------------------------------- /sample/src/main/res/drawable/shop3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/shop3.jpg -------------------------------------------------------------------------------- /sample/src/main/res/drawable/shop4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/shop4.jpg -------------------------------------------------------------------------------- /sample/src/main/res/drawable/shop5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/shop5.jpg -------------------------------------------------------------------------------- /sample/src/main/res/drawable/shop6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/drawable/shop6.jpg -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_gallery.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 25 | 26 | 33 | 34 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 29 | 30 | 37 | 38 | 45 | 46 | 47 | 48 | 52 | 53 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_shop.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 21 | 22 | 26 | 27 | 37 | 38 | 41 | 42 | 50 | 51 | 54 | 55 | 59 | 60 | 63 | 64 | 69 | 70 | 79 | 80 | 83 | 84 | 92 | 93 | 96 | 97 | 106 | 107 | 108 | 109 | 115 | 116 |