├── .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 | 
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 | 
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 | 
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 extends Activity> 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 |
126 |
127 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_weather.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
19 |
20 |
26 |
27 |
36 |
37 |
46 |
47 |
48 |
49 |
50 |
51 |
63 |
64 |
70 |
71 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/dialog_transition_time.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
19 |
20 |
30 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/item_city_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
32 |
33 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/item_gallery.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
19 |
20 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/item_shop_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/view_forecast.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
17 |
18 |
22 |
23 |
31 |
32 |
41 |
42 |
43 |
46 |
47 |
55 |
56 |
60 |
61 |
--------------------------------------------------------------------------------
/sample/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yarolegovich/DiscreteScrollView/a3a22d76c38e28cb03b45629b93e4ac03ce6fcc9/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/sample/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 | #000000
5 | #FF4081
6 |
7 | #727272
8 |
9 | #84ceca
10 | #727272
11 | #ffeb5a
12 | #51b2ac
13 |
14 | #212121
15 | @android:color/transparent
16 | #cc000000
17 | #b4ffffff
18 | #FFC107
19 |
20 | #84ceca
21 |
22 | #3453d1
23 | #6d69ff
24 | #cd47c6
25 |
26 |
27 |
- @color/gradientPeriodicClouds_c1
28 | - @color/gradientPeriodicClouds_c2
29 | - @color/gradientPeriodicClouds_c3
30 |
31 |
32 | #62bff5
33 | #61b3e5
34 | #5d8fb2
35 |
36 |
37 | - @color/gradientCloudy_c1
38 | - @color/gradientCloudy_c2
39 | - @color/gradientCloudy_c3
40 |
41 |
42 | #51bbf5
43 | #71c3ee
44 | #f3e5d2
45 |
46 |
47 | - @color/gradientMostlyCloudy_c1
48 | - @color/gradientMostlyCloudy_c2
49 | - @color/gradientMostlyCloudy_c3
50 |
51 |
52 | #5b64be
53 | #6084df
54 | #64a2ff
55 |
56 |
57 | - @color/gradientPartlyCloudy_c1
58 | - @color/gradientPartlyCloudy_c2
59 | - @color/gradientPartlyCloudy_c3
60 |
61 |
62 | #6d4fbe
63 | #6c56be
64 | #a189f1
65 |
66 |
67 | - @color/gradientClear_c1
68 | - @color/gradientClear_c2
69 | - @color/gradientClear_c3
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DiscreteScrollView Sample
3 |
4 | Preview
5 | Credits
6 |
7 | Big cards
8 | Small cards
9 | Vertical scroll
10 |
11 | Taisiya Yurkiv
12 | Zlatko Najdenovski, Freepik
13 | Herriottgrace
14 | Sample app design
15 | Monument icons from www.flaticon.com
16 | Shop goods photos from www.herriottgrace.com
17 |
18 | Smooth scroll
19 | Transition time
20 |
21 | Item transition time in millis
22 |
23 | No app found to open the url
24 | Unsupported operation
25 |
26 | pref_key_transition_time
27 |
28 | Rate on GitHub
29 |
30 | Back
31 | Comment
32 | Favourite
33 |
34 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
17 |
18 |
21 |
22 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample', ':library'
2 |
--------------------------------------------------------------------------------