├── .gitignore ├── CarouselLayoutManager ├── .gitignore ├── build.gradle ├── carousellayoutmanager │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── publish-module.gradle │ ├── publish-root.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── mig35 │ │ └── carousellayoutmanager │ │ ├── CarouselChildSelectionListener.java │ │ ├── CarouselLayoutManager.java │ │ ├── CarouselSmoothScroller.java │ │ ├── CarouselZoomPostLayoutListener.java │ │ ├── CenterScrollListener.java │ │ ├── DefaultChildSelectionListener.java │ │ └── ItemTransformation.java ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── mig35 │ │ │ └── carousellayoutmanager │ │ │ └── sample │ │ │ └── CarouselPreviewActivity.java │ │ └── res │ │ ├── layout │ │ ├── activity_carousel_preview.xml │ │ └── item_view.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 │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml └── settings.gradle ├── LICENSE ├── README.md └── resources ├── carousel_double_work_small.gif ├── carousel_work.gif ├── carousel_work_small.gif └── issues14.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | captures/ 33 | /CarouselLayoutManager/.idea 34 | -------------------------------------------------------------------------------- /CarouselLayoutManager/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /CarouselLayoutManager/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | maven { 6 | url "https://plugins.gradle.org/m2/" 7 | } 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:4.2.1' 11 | classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | task clean(type: Delete) { 23 | delete rootProject.buildDir 24 | } 25 | 26 | apply plugin: 'io.github.gradle-nexus.publish-plugin' -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.2.1' 9 | classpath "com.github.dcendents:android-maven-gradle-plugin:2.1" 10 | classpath "com.github.dcendents:android-maven-plugin:1.2" 11 | classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18" 12 | } 13 | } 14 | 15 | apply plugin: "com.android.library" 16 | apply plugin: 'com.github.dcendents.android-maven' 17 | 18 | final Properties properties = new Properties() 19 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 20 | 21 | android { 22 | compileSdkVersion 30 23 | resourcePrefix "carousellayoutmanager__" 24 | 25 | defaultConfig { 26 | minSdkVersion 11 27 | targetSdkVersion 30 28 | versionCode 1 29 | versionName version 30 | } 31 | buildTypes { 32 | release { 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 35 | } 36 | } 37 | } 38 | 39 | dependencies { 40 | implementation fileTree(include: ['*.jar'], dir: 'libs') 41 | 42 | implementation 'androidx.recyclerview:recyclerview:1.2.0' 43 | } 44 | 45 | ext { 46 | PUBLISH_GROUP_ID = 'com.mig35' 47 | PUBLISH_VERSION = '1.4.6' 48 | PUBLISH_ARTIFACT_ID = 'carousellayoutmanager' 49 | } 50 | 51 | apply from: "publish-root.gradle" 52 | apply from: "publish-module.gradle" -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/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\Mikhail\Documents\tools\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 | -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/publish-module.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | apply plugin: 'org.jetbrains.dokka' 4 | 5 | task androidSourcesJar(type: Jar) { 6 | archiveClassifier.set('sources') 7 | if (project.plugins.findPlugin("com.android.library")) { 8 | from android.sourceSets.main.java.srcDirs 9 | } else { 10 | from sourceSets.main.java.srcDirs 11 | } 12 | } 13 | 14 | //tasks.withType(dokkaHtmlPartial.getClass()).configureEach { 15 | // pluginsMapConfiguration.set( 16 | // ["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""] 17 | // ) 18 | //} 19 | 20 | task javadocJar(type: Jar) { 21 | archiveClassifier.set('javadoc') 22 | } 23 | 24 | artifacts { 25 | archives androidSourcesJar 26 | archives javadocJar 27 | } 28 | 29 | group = PUBLISH_GROUP_ID 30 | version = PUBLISH_VERSION 31 | 32 | afterEvaluate { 33 | publishing { 34 | publications { 35 | release(MavenPublication) { 36 | groupId PUBLISH_GROUP_ID 37 | artifactId PUBLISH_ARTIFACT_ID 38 | version PUBLISH_VERSION 39 | if (project.plugins.findPlugin("com.android.library")) { 40 | artifact bundleReleaseAar 41 | } else { 42 | artifact("$buildDir/libs/${project.getName()}-${version}.jar") 43 | } 44 | 45 | artifact androidSourcesJar 46 | artifact javadocJar 47 | 48 | pom { 49 | name = PUBLISH_ARTIFACT_ID 50 | description = 'Carousel Layout Manager for RecyclerView' 51 | url = 'https://github.com/Azoft/CarouselLayoutManager' 52 | licenses { 53 | license { 54 | name = 'License' 55 | url = 'https://github.com/Azoft/CarouselLayoutManager/blob/master/LICENSE' 56 | } 57 | } 58 | developers { 59 | developer { 60 | id = 'mig35' 61 | name = 'Mikhail Gurevich' 62 | email = 'mig35@mig35.com' 63 | } 64 | } 65 | scm { 66 | connection = 'scm:git:github.com/Azoft/CarouselLayoutManager.git' 67 | developerConnection = 'scm:git:ssh://github.com/Azoft/CarouselLayoutManager.git' 68 | url = 'https://github.com/Azoft/CarouselLayoutManager/tree/main' 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | signing { 77 | sign publishing.publications 78 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/publish-root.gradle: -------------------------------------------------------------------------------- 1 | ext["signing.keyId"] = '' 2 | ext["signing.password"] = '' 3 | ext["signing.secretKeyRingFile"] = '' 4 | ext["ossrhUsername"] = '' 5 | ext["ossrhPassword"] = '' 6 | ext["sonatypeStagingProfileId"] = '' 7 | 8 | File secretPropsFile = project.rootProject.file('local.properties') 9 | if (secretPropsFile.exists()) { 10 | // Read local.properties file first if it exists 11 | Properties p = new Properties() 12 | new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } 13 | p.each { name, value -> ext[name] = value } 14 | } else { 15 | // Use system environment variables 16 | ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') 17 | ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') 18 | ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') 19 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') 20 | ext["signing.password"] = System.getenv('SIGNING_PASSWORD') 21 | ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE') 22 | } 23 | 24 | // Set up Sonatype repository 25 | nexusPublishing { 26 | repositories { 27 | sonatype { 28 | stagingProfileId = sonatypeStagingProfileId 29 | username = ossrhUsername 30 | password = ossrhPassword 31 | 32 | /* Existing params here... */ 33 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 34 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/src/main/java/com/mig35/carousellayoutmanager/CarouselChildSelectionListener.java: -------------------------------------------------------------------------------- 1 | package com.mig35.carousellayoutmanager; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.recyclerview.widget.RecyclerView; 5 | import android.view.View; 6 | 7 | public abstract class CarouselChildSelectionListener { 8 | 9 | @NonNull 10 | private final RecyclerView mRecyclerView; 11 | @NonNull 12 | private final CarouselLayoutManager mCarouselLayoutManager; 13 | 14 | private final View.OnClickListener mOnClickListener = new View.OnClickListener() { 15 | @Override 16 | public void onClick(final View v) { 17 | final RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v); 18 | final int position = holder.getAdapterPosition(); 19 | 20 | if (position == mCarouselLayoutManager.getCenterItemPosition()) { 21 | onCenterItemClicked(mRecyclerView, mCarouselLayoutManager, v); 22 | } else { 23 | onBackItemClicked(mRecyclerView, mCarouselLayoutManager, v); 24 | } 25 | } 26 | }; 27 | 28 | protected CarouselChildSelectionListener(@NonNull final RecyclerView recyclerView, @NonNull final CarouselLayoutManager carouselLayoutManager) { 29 | mRecyclerView = recyclerView; 30 | mCarouselLayoutManager = carouselLayoutManager; 31 | 32 | mRecyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() { 33 | @Override 34 | public void onChildViewAttachedToWindow(@NonNull final View view) { 35 | view.setOnClickListener(mOnClickListener); 36 | } 37 | 38 | @Override 39 | public void onChildViewDetachedFromWindow(@NonNull final View view) { 40 | view.setOnClickListener(null); 41 | } 42 | }); 43 | } 44 | 45 | protected abstract void onCenterItemClicked(@NonNull final RecyclerView recyclerView, @NonNull final CarouselLayoutManager carouselLayoutManager, @NonNull final View v); 46 | 47 | protected abstract void onBackItemClicked(@NonNull final RecyclerView recyclerView, @NonNull final CarouselLayoutManager carouselLayoutManager, @NonNull final View v); 48 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/src/main/java/com/mig35/carousellayoutmanager/CarouselLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.mig35.carousellayoutmanager; 2 | 3 | import android.graphics.PointF; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.os.Parcel; 7 | import android.os.Parcelable; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import java.lang.ref.WeakReference; 12 | import java.util.ArrayList; 13 | import java.util.Iterator; 14 | import java.util.List; 15 | 16 | import androidx.annotation.CallSuper; 17 | import androidx.annotation.NonNull; 18 | import androidx.annotation.Nullable; 19 | import androidx.core.view.ViewCompat; 20 | import androidx.recyclerview.widget.LinearSmoothScroller; 21 | import androidx.recyclerview.widget.OrientationHelper; 22 | import androidx.recyclerview.widget.RecyclerView; 23 | 24 | /** 25 | * An implementation of {@link RecyclerView.LayoutManager} that layout items like carousel. 26 | * Generally there is one center item and bellow this item there are maximum {@link CarouselLayoutManager#getMaxVisibleItems()} items on each side of the center 27 | * item. By default {@link CarouselLayoutManager#getMaxVisibleItems()} is {@link CarouselLayoutManager#MAX_VISIBLE_ITEMS}.
28 | *
29 | * This LayoutManager supports only fixedSized adapter items.
30 | *
31 | * This LayoutManager supports {@link CarouselLayoutManager#HORIZONTAL} and {@link CarouselLayoutManager#VERTICAL} orientations.
32 | *
33 | * This LayoutManager supports circle layout. By default it if disabled. We don't recommend to use circle layout with adapter items count less then 3.
34 | *
35 | * Please be sure that layout_width of adapter item is a constant value and not {@link ViewGroup.LayoutParams#MATCH_PARENT} 36 | * for {@link #HORIZONTAL} orientation. 37 | * So like layout_height is not {@link ViewGroup.LayoutParams#MATCH_PARENT} for {@link CarouselLayoutManager#VERTICAL}
38 | *
39 | */ 40 | public class CarouselLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider { 41 | 42 | public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; 43 | public static final int VERTICAL = OrientationHelper.VERTICAL; 44 | 45 | public static final int INVALID_POSITION = -1; 46 | public static final int MAX_VISIBLE_ITEMS = 3; 47 | 48 | private static final boolean CIRCLE_LAYOUT = false; 49 | 50 | private boolean mDecoratedChildSizeInvalid; 51 | private Integer mDecoratedChildWidth; 52 | private Integer mDecoratedChildHeight; 53 | 54 | private final int mOrientation; 55 | private boolean mCircleLayout; 56 | 57 | private int mPendingScrollPosition; 58 | 59 | private final LayoutHelper mLayoutHelper = new LayoutHelper(MAX_VISIBLE_ITEMS); 60 | 61 | private PostLayoutListener mViewPostLayout; 62 | 63 | private final List mOnCenterItemSelectionListeners = new ArrayList<>(); 64 | private int mCenterItemPosition = INVALID_POSITION; 65 | private int mItemsCount; 66 | 67 | @Nullable 68 | private CarouselSavedState mPendingCarouselSavedState; 69 | 70 | /** 71 | * @param orientation should be {@link #VERTICAL} or {@link #HORIZONTAL} 72 | */ 73 | @SuppressWarnings("unused") 74 | public CarouselLayoutManager(final int orientation) { 75 | this(orientation, CIRCLE_LAYOUT); 76 | } 77 | 78 | /** 79 | * If circleLayout is true then all items will be in cycle. Scroll will be infinite on both sides. 80 | * 81 | * @param orientation should be {@link #VERTICAL} or {@link #HORIZONTAL} 82 | * @param circleLayout true for enabling circleLayout 83 | */ 84 | @SuppressWarnings("unused") 85 | public CarouselLayoutManager(final int orientation, final boolean circleLayout) { 86 | if (HORIZONTAL != orientation && VERTICAL != orientation) { 87 | throw new IllegalArgumentException("orientation should be HORIZONTAL or VERTICAL"); 88 | } 89 | mOrientation = orientation; 90 | mCircleLayout = circleLayout; 91 | mPendingScrollPosition = INVALID_POSITION; 92 | } 93 | 94 | /** 95 | * Change circle layout type 96 | */ 97 | @SuppressWarnings("unused") 98 | public void setCircleLayout(final boolean circleLayout) { 99 | if (mCircleLayout != circleLayout) { 100 | mCircleLayout = circleLayout; 101 | requestLayout(); 102 | } 103 | } 104 | 105 | /** 106 | * Setup {@link CarouselLayoutManager.PostLayoutListener} for this LayoutManager. 107 | * Its methods will be called for each visible view item after general LayoutManager layout finishes.
108 | *
109 | * Generally this method should be used for scaling and translating view item for better (different) view presentation of layouting. 110 | * 111 | * @param postLayoutListener listener for item layout changes. Can be null. 112 | */ 113 | @SuppressWarnings("unused") 114 | public void setPostLayoutListener(@Nullable final PostLayoutListener postLayoutListener) { 115 | mViewPostLayout = postLayoutListener; 116 | requestLayout(); 117 | } 118 | 119 | /** 120 | * Setup maximum visible (layout) items on each side of the center item. 121 | * Basically during scrolling there can be more visible items (+1 item on each side), but in idle state this is the only reached maximum. 122 | * 123 | * @param maxVisibleItems should be great then 0, if bot an {@link IllegalAccessException} will be thrown 124 | */ 125 | @CallSuper 126 | @SuppressWarnings("unused") 127 | public void setMaxVisibleItems(final int maxVisibleItems) { 128 | if (0 > maxVisibleItems) { 129 | throw new IllegalArgumentException("maxVisibleItems can't be less then 0"); 130 | } 131 | mLayoutHelper.mMaxVisibleItems = maxVisibleItems; 132 | requestLayout(); 133 | } 134 | 135 | /** 136 | * @return current setup for maximum visible items. 137 | * @see #setMaxVisibleItems(int) 138 | */ 139 | @SuppressWarnings("unused") 140 | public int getMaxVisibleItems() { 141 | return mLayoutHelper.mMaxVisibleItems; 142 | } 143 | 144 | @Override 145 | public RecyclerView.LayoutParams generateDefaultLayoutParams() { 146 | return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 147 | } 148 | 149 | /** 150 | * @return current layout orientation 151 | * @see #VERTICAL 152 | * @see #HORIZONTAL 153 | */ 154 | public int getOrientation() { 155 | return mOrientation; 156 | } 157 | 158 | @Override 159 | public boolean canScrollHorizontally() { 160 | return 0 != getChildCount() && HORIZONTAL == mOrientation; 161 | } 162 | 163 | @Override 164 | public boolean canScrollVertically() { 165 | return 0 != getChildCount() && VERTICAL == mOrientation; 166 | } 167 | 168 | /** 169 | * @return current layout center item 170 | */ 171 | public int getCenterItemPosition() { 172 | return mCenterItemPosition; 173 | } 174 | 175 | /** 176 | * @param onCenterItemSelectionListener listener that will trigger when ItemSelectionChanges. can't be null 177 | */ 178 | public void addOnItemSelectionListener(@NonNull final OnCenterItemSelectionListener onCenterItemSelectionListener) { 179 | mOnCenterItemSelectionListeners.add(onCenterItemSelectionListener); 180 | } 181 | 182 | /** 183 | * @param onCenterItemSelectionListener listener that was previously added by {@link #addOnItemSelectionListener(OnCenterItemSelectionListener)} 184 | */ 185 | public void removeOnItemSelectionListener(@NonNull final OnCenterItemSelectionListener onCenterItemSelectionListener) { 186 | mOnCenterItemSelectionListeners.remove(onCenterItemSelectionListener); 187 | } 188 | 189 | @SuppressWarnings("RefusedBequest") 190 | @Override 191 | public void scrollToPosition(final int position) { 192 | if (0 > position) { 193 | throw new IllegalArgumentException("position can't be less then 0. position is : " + position); 194 | } 195 | mPendingScrollPosition = position; 196 | requestLayout(); 197 | } 198 | 199 | @SuppressWarnings("RefusedBequest") 200 | @Override 201 | public void smoothScrollToPosition(@NonNull final RecyclerView recyclerView, @NonNull final RecyclerView.State state, final int position) { 202 | final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { 203 | @Override 204 | public int calculateDyToMakeVisible(final View view, final int snapPreference) { 205 | if (!canScrollVertically()) { 206 | return 0; 207 | } 208 | 209 | return getOffsetForCurrentView(view); 210 | } 211 | 212 | @Override 213 | public int calculateDxToMakeVisible(final View view, final int snapPreference) { 214 | if (!canScrollHorizontally()) { 215 | return 0; 216 | } 217 | return getOffsetForCurrentView(view); 218 | } 219 | }; 220 | linearSmoothScroller.setTargetPosition(position); 221 | startSmoothScroll(linearSmoothScroller); 222 | } 223 | 224 | @Override 225 | @Nullable 226 | public PointF computeScrollVectorForPosition(final int targetPosition) { 227 | if (0 == getChildCount()) { 228 | return null; 229 | } 230 | final float directionDistance = getScrollDirection(targetPosition); 231 | //noinspection NumericCastThatLosesPrecision 232 | final int direction = (int) -Math.signum(directionDistance); 233 | 234 | if (HORIZONTAL == mOrientation) { 235 | return new PointF(direction, 0); 236 | } else { 237 | return new PointF(0, direction); 238 | } 239 | } 240 | 241 | private float getScrollDirection(final int targetPosition) { 242 | final float currentScrollPosition = makeScrollPositionInRange0ToCount(getCurrentScrollPosition(), mItemsCount); 243 | 244 | if (mCircleLayout) { 245 | final float t1 = currentScrollPosition - targetPosition; 246 | final float t2 = Math.abs(t1) - mItemsCount; 247 | if (Math.abs(t1) > Math.abs(t2)) { 248 | return Math.signum(t1) * t2; 249 | } else { 250 | return t1; 251 | } 252 | } else { 253 | return currentScrollPosition - targetPosition; 254 | } 255 | } 256 | 257 | @Override 258 | public int scrollVerticallyBy(final int dy, @NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) { 259 | if (HORIZONTAL == mOrientation) { 260 | return 0; 261 | } 262 | return scrollBy(dy, recycler, state); 263 | } 264 | 265 | @Override 266 | public int scrollHorizontallyBy(final int dx, final RecyclerView.Recycler recycler, final RecyclerView.State state) { 267 | if (VERTICAL == mOrientation) { 268 | return 0; 269 | } 270 | return scrollBy(dx, recycler, state); 271 | } 272 | 273 | /** 274 | * This method is called from {@link #scrollHorizontallyBy(int, RecyclerView.Recycler, RecyclerView.State)} and 275 | * {@link #scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State)} to calculate needed scroll that is allowed.
276 | *
277 | * This method may do relayout work. 278 | * 279 | * @param diff distance that we want to scroll by 280 | * @param recycler Recycler to use for fetching potentially cached views for a position 281 | * @param state Transient state of RecyclerView 282 | * @return distance that we actually scrolled by 283 | */ 284 | @CallSuper 285 | protected int scrollBy(final int diff, @NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) { 286 | if (null == mDecoratedChildWidth || null == mDecoratedChildHeight) { 287 | return 0; 288 | } 289 | if (0 == getChildCount() || 0 == diff) { 290 | return 0; 291 | } 292 | final int resultScroll; 293 | if (mCircleLayout) { 294 | resultScroll = diff; 295 | 296 | mLayoutHelper.mScrollOffset += resultScroll; 297 | 298 | final int maxOffset = getScrollItemSize() * mItemsCount; 299 | while (0 > mLayoutHelper.mScrollOffset) { 300 | mLayoutHelper.mScrollOffset += maxOffset; 301 | } 302 | while (mLayoutHelper.mScrollOffset > maxOffset) { 303 | mLayoutHelper.mScrollOffset -= maxOffset; 304 | } 305 | 306 | mLayoutHelper.mScrollOffset -= resultScroll; 307 | } else { 308 | final int maxOffset = getMaxScrollOffset(); 309 | 310 | if (0 > mLayoutHelper.mScrollOffset + diff) { 311 | resultScroll = -mLayoutHelper.mScrollOffset; //to make it 0 312 | } else if (mLayoutHelper.mScrollOffset + diff > maxOffset) { 313 | resultScroll = maxOffset - mLayoutHelper.mScrollOffset; //to make it maxOffset 314 | } else { 315 | resultScroll = diff; 316 | } 317 | } 318 | if (0 != resultScroll) { 319 | mLayoutHelper.mScrollOffset += resultScroll; 320 | fillData(recycler, state); 321 | } 322 | return resultScroll; 323 | } 324 | 325 | @Override 326 | public void onMeasure(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state, final int widthSpec, final int heightSpec) { 327 | mDecoratedChildSizeInvalid = true; 328 | 329 | super.onMeasure(recycler, state, widthSpec, heightSpec); 330 | } 331 | 332 | @SuppressWarnings("rawtypes") 333 | @Override 334 | public void onAdapterChanged(final RecyclerView.Adapter oldAdapter, final RecyclerView.Adapter newAdapter) { 335 | super.onAdapterChanged(oldAdapter, newAdapter); 336 | 337 | removeAllViews(); 338 | } 339 | 340 | @SuppressWarnings("RefusedBequest") 341 | @Override 342 | @CallSuper 343 | public void onLayoutChildren(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) { 344 | if (0 == state.getItemCount()) { 345 | removeAndRecycleAllViews(recycler); 346 | selectItemCenterPosition(INVALID_POSITION); 347 | return; 348 | } 349 | 350 | detachAndScrapAttachedViews(recycler); 351 | 352 | if (null == mDecoratedChildWidth || mDecoratedChildSizeInvalid) { 353 | final List scrapList = recycler.getScrapList(); 354 | 355 | final boolean shouldRecycle; 356 | final View view; 357 | if (scrapList.isEmpty()) { 358 | shouldRecycle = true; 359 | final int itemsCount = state.getItemCount(); 360 | view = recycler.getViewForPosition( 361 | mPendingScrollPosition == INVALID_POSITION ? 362 | 0 : 363 | Math.max(0, Math.min(itemsCount - 1, mPendingScrollPosition)) 364 | ); 365 | addView(view); 366 | } else { 367 | shouldRecycle = false; 368 | view = scrapList.get(0).itemView; 369 | } 370 | measureChildWithMargins(view, 0, 0); 371 | 372 | final int decoratedChildWidth = getDecoratedMeasuredWidth(view); 373 | final int decoratedChildHeight = getDecoratedMeasuredHeight(view); 374 | if (shouldRecycle) { 375 | detachAndScrapView(view, recycler); 376 | } 377 | 378 | if (null != mDecoratedChildWidth && (mDecoratedChildWidth != decoratedChildWidth || mDecoratedChildHeight != decoratedChildHeight)) { 379 | if (INVALID_POSITION == mPendingScrollPosition && null == mPendingCarouselSavedState) { 380 | mPendingScrollPosition = mCenterItemPosition; 381 | } 382 | } 383 | 384 | mDecoratedChildWidth = decoratedChildWidth; 385 | mDecoratedChildHeight = decoratedChildHeight; 386 | mDecoratedChildSizeInvalid = false; 387 | } 388 | 389 | if (INVALID_POSITION != mPendingScrollPosition) { 390 | final int itemsCount = state.getItemCount(); 391 | mPendingScrollPosition = 0 == itemsCount ? INVALID_POSITION : Math.max(0, Math.min(itemsCount - 1, mPendingScrollPosition)); 392 | } 393 | if (INVALID_POSITION != mPendingScrollPosition) { 394 | mLayoutHelper.mScrollOffset = calculateScrollForSelectingPosition(mPendingScrollPosition, state); 395 | mPendingScrollPosition = INVALID_POSITION; 396 | mPendingCarouselSavedState = null; 397 | } else if (null != mPendingCarouselSavedState) { 398 | mLayoutHelper.mScrollOffset = calculateScrollForSelectingPosition(mPendingCarouselSavedState.mCenterItemPosition, state); 399 | mPendingCarouselSavedState = null; 400 | } else if (state.didStructureChange() && INVALID_POSITION != mCenterItemPosition) { 401 | mLayoutHelper.mScrollOffset = calculateScrollForSelectingPosition(mCenterItemPosition, state); 402 | } 403 | 404 | fillData(recycler, state); 405 | } 406 | 407 | private int calculateScrollForSelectingPosition(final int itemPosition, final RecyclerView.State state) { 408 | if (itemPosition == INVALID_POSITION) { 409 | return 0; 410 | } 411 | 412 | final int fixedItemPosition = itemPosition < state.getItemCount() ? itemPosition : state.getItemCount() - 1; 413 | return fixedItemPosition * (VERTICAL == mOrientation ? mDecoratedChildHeight : mDecoratedChildWidth); 414 | } 415 | 416 | private void fillData(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) { 417 | final float currentScrollPosition = getCurrentScrollPosition(); 418 | 419 | generateLayoutOrder(currentScrollPosition, state); 420 | detachAndScrapAttachedViews(recycler); 421 | recyclerOldViews(recycler); 422 | 423 | final int width = getWidthNoPadding(); 424 | final int height = getHeightNoPadding(); 425 | if (VERTICAL == mOrientation) { 426 | fillDataVertical(recycler, width, height); 427 | } else { 428 | fillDataHorizontal(recycler, width, height); 429 | } 430 | 431 | recycler.clear(); 432 | 433 | detectOnItemSelectionChanged(currentScrollPosition, state); 434 | } 435 | 436 | private void detectOnItemSelectionChanged(final float currentScrollPosition, final RecyclerView.State state) { 437 | final float absCurrentScrollPosition = makeScrollPositionInRange0ToCount(currentScrollPosition, state.getItemCount()); 438 | final int centerItem = Math.round(absCurrentScrollPosition); 439 | 440 | if (mCenterItemPosition != centerItem) { 441 | mCenterItemPosition = centerItem; 442 | new Handler(Looper.getMainLooper()).post(new Runnable() { 443 | @Override 444 | public void run() { 445 | selectItemCenterPosition(centerItem); 446 | } 447 | }); 448 | } 449 | } 450 | 451 | private void selectItemCenterPosition(final int centerItem) { 452 | for (final OnCenterItemSelectionListener onCenterItemSelectionListener : mOnCenterItemSelectionListeners) { 453 | onCenterItemSelectionListener.onCenterItemChanged(centerItem); 454 | } 455 | } 456 | 457 | private void fillDataVertical(final RecyclerView.Recycler recycler, final int width, final int height) { 458 | final int start = (width - mDecoratedChildWidth) / 2; 459 | final int end = start + mDecoratedChildWidth; 460 | 461 | final int centerViewTop = (height - mDecoratedChildHeight) / 2; 462 | 463 | for (int i = 0, count = mLayoutHelper.mLayoutOrder.length; i < count; ++i) { 464 | final LayoutOrder layoutOrder = mLayoutHelper.mLayoutOrder[i]; 465 | final int offset = getCardOffsetByPositionDiff(layoutOrder.mItemPositionDiff); 466 | final int top = centerViewTop + offset; 467 | final int bottom = top + mDecoratedChildHeight; 468 | fillChildItem(start, top, end, bottom, layoutOrder, recycler, i); 469 | } 470 | } 471 | 472 | private void fillDataHorizontal(final RecyclerView.Recycler recycler, final int width, final int height) { 473 | final int top = (height - mDecoratedChildHeight) / 2; 474 | final int bottom = top + mDecoratedChildHeight; 475 | 476 | final int centerViewStart = (width - mDecoratedChildWidth) / 2; 477 | 478 | for (int i = 0, count = mLayoutHelper.mLayoutOrder.length; i < count; ++i) { 479 | final LayoutOrder layoutOrder = mLayoutHelper.mLayoutOrder[i]; 480 | final int offset = getCardOffsetByPositionDiff(layoutOrder.mItemPositionDiff); 481 | final int start = centerViewStart + offset; 482 | final int end = start + mDecoratedChildWidth; 483 | fillChildItem(start, top, end, bottom, layoutOrder, recycler, i); 484 | } 485 | } 486 | 487 | 488 | @SuppressWarnings("MethodWithTooManyParameters") 489 | private void fillChildItem(final int start, final int top, final int end, final int bottom, @NonNull final LayoutOrder layoutOrder, @NonNull final RecyclerView.Recycler recycler, final int i) { 490 | final View view = bindChild(layoutOrder.mItemAdapterPosition, recycler); 491 | ViewCompat.setElevation(view, i); 492 | ItemTransformation transformation = null; 493 | if (null != mViewPostLayout) { 494 | transformation = mViewPostLayout.transformChild(view, layoutOrder.mItemPositionDiff, mOrientation, layoutOrder.mItemAdapterPosition); 495 | } 496 | if (null == transformation) { 497 | view.layout(start, top, end, bottom); 498 | } else { 499 | view.layout(Math.round(start + transformation.mTranslationX), Math.round(top + transformation.mTranslationY), 500 | Math.round(end + transformation.mTranslationX), Math.round(bottom + transformation.mTranslationY)); 501 | 502 | view.setScaleX(transformation.mScaleX); 503 | view.setScaleY(transformation.mScaleY); 504 | } 505 | } 506 | 507 | /** 508 | * @return current scroll position of center item. this value can be in any range if it is cycle layout. 509 | * if this is not, that then it is in [0, {@link #mItemsCount - 1}] 510 | */ 511 | private float getCurrentScrollPosition() { 512 | final int fullScrollSize = getMaxScrollOffset(); 513 | if (0 == fullScrollSize) { 514 | return 0; 515 | } 516 | return 1.0f * mLayoutHelper.mScrollOffset / getScrollItemSize(); 517 | } 518 | 519 | /** 520 | * @return maximum scroll value to fill up all items in layout. Generally this is only needed for non cycle layouts. 521 | */ 522 | private int getMaxScrollOffset() { 523 | return getScrollItemSize() * (mItemsCount - 1); 524 | } 525 | 526 | /** 527 | * Because we can support old Android versions, we should layout our children in specific order to make our center view in the top of layout 528 | * (this item should layout last). So this method will calculate layout order and fill up {@link #mLayoutHelper} object. 529 | * This object will be filled by only needed to layout items. Non visible items will not be there. 530 | * 531 | * @param currentScrollPosition current scroll position this is a value that indicates position of center item 532 | * (if this value is int, then center item is really in the center of the layout, else it is near state). 533 | * Be aware that this value can be in any range is it is cycle layout 534 | * @param state Transient state of RecyclerView 535 | * @see #getCurrentScrollPosition() 536 | */ 537 | private void generateLayoutOrder(final float currentScrollPosition, @NonNull final RecyclerView.State state) { 538 | mItemsCount = state.getItemCount(); 539 | final float absCurrentScrollPosition = makeScrollPositionInRange0ToCount(currentScrollPosition, mItemsCount); 540 | final int centerItem = Math.round(absCurrentScrollPosition); 541 | 542 | if (mCircleLayout && 1 < mItemsCount) { 543 | final int layoutCount = Math.min(mLayoutHelper.mMaxVisibleItems * 2 + 1, mItemsCount); 544 | 545 | mLayoutHelper.initLayoutOrder(layoutCount); 546 | 547 | final int countLayoutHalf = layoutCount / 2; 548 | // before center item 549 | for (int i = 1; i <= countLayoutHalf; ++i) { 550 | final int position = Math.round(absCurrentScrollPosition - i + mItemsCount) % mItemsCount; 551 | mLayoutHelper.setLayoutOrder(countLayoutHalf - i, position, centerItem - absCurrentScrollPosition - i); 552 | } 553 | // after center item 554 | for (int i = layoutCount - 1; i >= countLayoutHalf + 1; --i) { 555 | final int position = Math.round(absCurrentScrollPosition - i + layoutCount) % mItemsCount; 556 | mLayoutHelper.setLayoutOrder(i - 1, position, centerItem - absCurrentScrollPosition + layoutCount - i); 557 | } 558 | mLayoutHelper.setLayoutOrder(layoutCount - 1, centerItem, centerItem - absCurrentScrollPosition); 559 | 560 | } else { 561 | final int firstVisible = Math.max(centerItem - mLayoutHelper.mMaxVisibleItems, 0); 562 | final int lastVisible = Math.min(centerItem + mLayoutHelper.mMaxVisibleItems, mItemsCount - 1); 563 | final int layoutCount = lastVisible - firstVisible + 1; 564 | 565 | mLayoutHelper.initLayoutOrder(layoutCount); 566 | 567 | for (int i = firstVisible; i <= lastVisible; ++i) { 568 | if (i == centerItem) { 569 | mLayoutHelper.setLayoutOrder(layoutCount - 1, i, i - absCurrentScrollPosition); 570 | } else if (i < centerItem) { 571 | mLayoutHelper.setLayoutOrder(i - firstVisible, i, i - absCurrentScrollPosition); 572 | } else { 573 | mLayoutHelper.setLayoutOrder(layoutCount - (i - centerItem) - 1, i, i - absCurrentScrollPosition); 574 | } 575 | } 576 | } 577 | } 578 | 579 | public int getWidthNoPadding() { 580 | return getWidth() - getPaddingStart() - getPaddingEnd(); 581 | } 582 | 583 | public int getHeightNoPadding() { 584 | return getHeight() - getPaddingEnd() - getPaddingStart(); 585 | } 586 | 587 | private View bindChild(final int position, @NonNull final RecyclerView.Recycler recycler) { 588 | final View view = recycler.getViewForPosition(position); 589 | 590 | addView(view); 591 | measureChildWithMargins(view, 0, 0); 592 | 593 | return view; 594 | } 595 | 596 | private void recyclerOldViews(final RecyclerView.Recycler recycler) { 597 | for (RecyclerView.ViewHolder viewHolder : new ArrayList<>(recycler.getScrapList())) { 598 | int adapterPosition = viewHolder.getAdapterPosition(); 599 | boolean found = false; 600 | for (LayoutOrder layoutOrder : mLayoutHelper.mLayoutOrder) { 601 | if (layoutOrder.mItemAdapterPosition == adapterPosition) { 602 | found = true; 603 | break; 604 | } 605 | } 606 | if (!found) { 607 | recycler.recycleView(viewHolder.itemView); 608 | } 609 | } 610 | } 611 | 612 | /** 613 | * Called during {@link #fillData(RecyclerView.Recycler, RecyclerView.State)} to calculate item offset from layout center line.
614 | *
615 | * Returns {@link #convertItemPositionDiffToSmoothPositionDiff(float)} * (size off area above center item when it is on the center).
616 | * Sign is: plus if this item is bellow center line, minus if not
617 | *
618 | * ----- - area above it
619 | * ||||| - center item
620 | * ----- - area bellow it (it has the same size as are above center item)
621 | * 622 | * @param itemPositionDiff current item difference with layout center line. if this is 0, then this item center is in layout center line. 623 | * if this is 1 then this item is bellow the layout center line in the full item size distance. 624 | * @return offset in scroll px coordinates. 625 | */ 626 | protected int getCardOffsetByPositionDiff(final float itemPositionDiff) { 627 | final double smoothPosition = convertItemPositionDiffToSmoothPositionDiff(itemPositionDiff); 628 | 629 | final int dimenDiff; 630 | if (VERTICAL == mOrientation) { 631 | dimenDiff = (getHeightNoPadding() - mDecoratedChildHeight) / 2; 632 | } else { 633 | dimenDiff = (getWidthNoPadding() - mDecoratedChildWidth) / 2; 634 | } 635 | //noinspection NumericCastThatLosesPrecision 636 | return (int) Math.round(Math.signum(itemPositionDiff) * dimenDiff * smoothPosition); 637 | } 638 | 639 | /** 640 | * Called during {@link #getCardOffsetByPositionDiff(float)} for better item movement.
641 | * Current implementation speed up items that are far from layout center line and slow down items that are close to this line. 642 | * This code is full of maths. If you want to make items move in a different way, probably you should override this method.
643 | * Please see code comments for better explanations. 644 | * 645 | * @param itemPositionDiff current item difference with layout center line. if this is 0, then this item center is in layout center line. 646 | * if this is 1 then this item is bellow the layout center line in the full item size distance. 647 | * @return smooth position offset. needed for scroll calculation and better user experience. 648 | * @see #getCardOffsetByPositionDiff(float) 649 | */ 650 | @SuppressWarnings({"MagicNumber", "InstanceMethodNamingConvention"}) 651 | protected double convertItemPositionDiffToSmoothPositionDiff(final float itemPositionDiff) { 652 | // generally item moves the same way above center and bellow it. So we don't care about diff sign. 653 | final float absIemPositionDiff = Math.abs(itemPositionDiff); 654 | 655 | // we detect if this item is close for center or not. We use (1 / maxVisibleItem) ^ (1/3) as close definer. 656 | if (absIemPositionDiff > StrictMath.pow(1.0f / mLayoutHelper.mMaxVisibleItems, 1.0f / 3)) { 657 | // this item is far from center line, so we should make it move like square root function 658 | return StrictMath.pow(absIemPositionDiff / mLayoutHelper.mMaxVisibleItems, 1 / 2.0f); 659 | } else { 660 | // this item is close from center line. we should slow it down and don't make it speed up very quick. 661 | // so square function in range of [0, (1/maxVisible)^(1/3)] is quite good in it; 662 | return StrictMath.pow(absIemPositionDiff, 2.0f); 663 | } 664 | } 665 | 666 | /** 667 | * @return full item size 668 | */ 669 | protected int getScrollItemSize() { 670 | if (VERTICAL == mOrientation) { 671 | return mDecoratedChildHeight; 672 | } else { 673 | return mDecoratedChildWidth; 674 | } 675 | } 676 | 677 | @Override 678 | public Parcelable onSaveInstanceState() { 679 | if (null != mPendingCarouselSavedState) { 680 | return new CarouselSavedState(mPendingCarouselSavedState); 681 | } 682 | final CarouselSavedState savedState = new CarouselSavedState(super.onSaveInstanceState()); 683 | savedState.mCenterItemPosition = mCenterItemPosition; 684 | return savedState; 685 | } 686 | 687 | @Override 688 | public void onRestoreInstanceState(final Parcelable state) { 689 | if (state instanceof CarouselSavedState) { 690 | mPendingCarouselSavedState = (CarouselSavedState) state; 691 | 692 | super.onRestoreInstanceState(mPendingCarouselSavedState.mSuperState); 693 | } else { 694 | super.onRestoreInstanceState(state); 695 | } 696 | } 697 | 698 | /** 699 | * @return Scroll offset from nearest item from center 700 | */ 701 | protected int getOffsetCenterView() { 702 | return Math.round(getCurrentScrollPosition()) * getScrollItemSize() - mLayoutHelper.mScrollOffset; 703 | } 704 | 705 | protected int getOffsetForCurrentView(@NonNull final View view) { 706 | final int targetPosition = getPosition(view); 707 | final float directionDistance = getScrollDirection(targetPosition); 708 | 709 | return Math.round(directionDistance * getScrollItemSize()); 710 | } 711 | 712 | /** 713 | * Helper method that make scroll in range of [0, count). Generally this method is needed only for cycle layout. 714 | * 715 | * @param currentScrollPosition any scroll position range. 716 | * @param count adapter items count 717 | * @return good scroll position in range of [0, count) 718 | */ 719 | private static float makeScrollPositionInRange0ToCount(final float currentScrollPosition, final int count) { 720 | float absCurrentScrollPosition = currentScrollPosition; 721 | while (0 > absCurrentScrollPosition) { 722 | absCurrentScrollPosition += count; 723 | } 724 | while (Math.round(absCurrentScrollPosition) >= count) { 725 | absCurrentScrollPosition -= count; 726 | } 727 | return absCurrentScrollPosition; 728 | } 729 | 730 | /** 731 | * This interface methods will be called for each visible view item after general LayoutManager layout finishes.
732 | *
733 | * Generally this method should be used for scaling and translating view item for better (different) view presentation of layouting. 734 | */ 735 | @SuppressWarnings("InterfaceNeverImplemented") 736 | public abstract static class PostLayoutListener { 737 | 738 | /** 739 | * Called after child layout finished. Generally you can do any translation and scaling work here. 740 | * 741 | * @param child view that was layout 742 | * @param itemPositionToCenterDiff view center line difference to layout center. if > 0 then this item is bellow layout center line, else if not 743 | * @param orientation layoutManager orientation {@link #getLayoutDirection()} 744 | * @param itemPositionInAdapter item position inside adapter for this layout pass 745 | */ 746 | public ItemTransformation transformChild( 747 | @NonNull final View child, 748 | final float itemPositionToCenterDiff, 749 | final int orientation, 750 | final int itemPositionInAdapter 751 | ) { 752 | return transformChild(child, itemPositionToCenterDiff, orientation); 753 | } 754 | 755 | /** 756 | * Called after child layout finished. Generally you can do any translation and scaling work here. 757 | * 758 | * @param child view that was layout 759 | * @param itemPositionToCenterDiff view center line difference to layout center. if > 0 then this item is bellow layout center line, else if not 760 | * @param orientation layoutManager orientation {@link #getLayoutDirection()} 761 | */ 762 | public ItemTransformation transformChild( 763 | @NonNull final View child, 764 | final float itemPositionToCenterDiff, 765 | final int orientation 766 | ) { 767 | throw new IllegalStateException("at least one transformChild should be implemented"); 768 | } 769 | } 770 | 771 | public interface OnCenterItemSelectionListener { 772 | 773 | /** 774 | * Listener that will be called on every change of center item. 775 | * This listener will be triggered on every layout operation if item was changed. 776 | * Do not do any expensive operations in this method since this will effect scroll experience. 777 | * 778 | * @param adapterPosition current layout center item 779 | */ 780 | void onCenterItemChanged(final int adapterPosition); 781 | } 782 | 783 | /** 784 | * Helper class that holds currently visible items. 785 | * Generally this class fills this list.
786 | *
787 | * This class holds all scroll and maxVisible items state. 788 | * 789 | * @see #getMaxVisibleItems() 790 | */ 791 | private static class LayoutHelper { 792 | 793 | private int mMaxVisibleItems; 794 | 795 | private int mScrollOffset; 796 | 797 | private LayoutOrder[] mLayoutOrder; 798 | 799 | private final List> mReusedItems = new ArrayList<>(); 800 | 801 | LayoutHelper(final int maxVisibleItems) { 802 | mMaxVisibleItems = maxVisibleItems; 803 | } 804 | 805 | /** 806 | * Called before any fill calls. Needed to recycle old items and init new array list. Generally this list is an array an it is reused. 807 | * 808 | * @param layoutCount items count that will be layout 809 | */ 810 | void initLayoutOrder(final int layoutCount) { 811 | if (null == mLayoutOrder || mLayoutOrder.length != layoutCount) { 812 | if (null != mLayoutOrder) { 813 | recycleItems(mLayoutOrder); 814 | } 815 | mLayoutOrder = new LayoutOrder[layoutCount]; 816 | fillLayoutOrder(); 817 | } 818 | } 819 | 820 | /** 821 | * Called during layout generation process of filling this list. Should be called only after {@link #initLayoutOrder(int)} method call. 822 | * 823 | * @param arrayPosition position in layout order 824 | * @param itemAdapterPosition adapter position of item for future data filling logic 825 | * @param itemPositionDiff difference of current item scroll position and center item position. 826 | * if this is a center item and it is in real center of layout, then this will be 0. 827 | * if current layout is not in the center, then this value will never be int. 828 | * if this item center is bellow layout center line then this value is greater then 0, 829 | * else less then 0. 830 | */ 831 | void setLayoutOrder(final int arrayPosition, final int itemAdapterPosition, final float itemPositionDiff) { 832 | final LayoutOrder item = mLayoutOrder[arrayPosition]; 833 | item.mItemAdapterPosition = itemAdapterPosition; 834 | item.mItemPositionDiff = itemPositionDiff; 835 | } 836 | 837 | /** 838 | * Checks is this screen Layout has this adapterPosition view in layout 839 | * 840 | * @param adapterPosition adapter position of item for future data filling logic 841 | * @return true is adapterItem is in layout 842 | */ 843 | boolean hasAdapterPosition(final int adapterPosition) { 844 | if (null != mLayoutOrder) { 845 | for (final LayoutOrder layoutOrder : mLayoutOrder) { 846 | if (layoutOrder.mItemAdapterPosition == adapterPosition) { 847 | return true; 848 | } 849 | } 850 | } 851 | return false; 852 | } 853 | 854 | @SuppressWarnings("VariableArgumentMethod") 855 | private void recycleItems(@NonNull final LayoutOrder... layoutOrders) { 856 | for (final LayoutOrder layoutOrder : layoutOrders) { 857 | //noinspection ObjectAllocationInLoop 858 | mReusedItems.add(new WeakReference<>(layoutOrder)); 859 | } 860 | } 861 | 862 | private void fillLayoutOrder() { 863 | for (int i = 0, length = mLayoutOrder.length; i < length; ++i) { 864 | if (null == mLayoutOrder[i]) { 865 | mLayoutOrder[i] = createLayoutOrder(); 866 | } 867 | } 868 | } 869 | 870 | private LayoutOrder createLayoutOrder() { 871 | final Iterator> iterator = mReusedItems.iterator(); 872 | while (iterator.hasNext()) { 873 | final WeakReference layoutOrderWeakReference = iterator.next(); 874 | final LayoutOrder layoutOrder = layoutOrderWeakReference.get(); 875 | iterator.remove(); 876 | if (null != layoutOrder) { 877 | return layoutOrder; 878 | } 879 | } 880 | return new LayoutOrder(); 881 | } 882 | } 883 | 884 | /** 885 | * Class that holds item data. 886 | * This class is filled during {@link #generateLayoutOrder(float, RecyclerView.State)} and used during {@link #fillData(RecyclerView.Recycler, RecyclerView.State)} 887 | */ 888 | private static class LayoutOrder { 889 | 890 | /** 891 | * Item adapter position 892 | */ 893 | private int mItemAdapterPosition; 894 | /** 895 | * Item center difference to layout center. If center of item is bellow layout center, then this value is greater then 0, else it is less. 896 | */ 897 | private float mItemPositionDiff; 898 | } 899 | 900 | protected static class CarouselSavedState implements Parcelable { 901 | 902 | private final Parcelable mSuperState; 903 | private int mCenterItemPosition; 904 | 905 | protected CarouselSavedState(@Nullable final Parcelable superState) { 906 | mSuperState = superState; 907 | } 908 | 909 | private CarouselSavedState(@NonNull final Parcel in) { 910 | mSuperState = in.readParcelable(Parcelable.class.getClassLoader()); 911 | mCenterItemPosition = in.readInt(); 912 | } 913 | 914 | protected CarouselSavedState(@NonNull final CarouselSavedState other) { 915 | mSuperState = other.mSuperState; 916 | mCenterItemPosition = other.mCenterItemPosition; 917 | } 918 | 919 | @Override 920 | public int describeContents() { 921 | return 0; 922 | } 923 | 924 | @Override 925 | public void writeToParcel(final Parcel parcel, final int i) { 926 | parcel.writeParcelable(mSuperState, i); 927 | parcel.writeInt(mCenterItemPosition); 928 | } 929 | 930 | public static final Parcelable.Creator CREATOR 931 | = new Parcelable.Creator() { 932 | @Override 933 | public CarouselSavedState createFromParcel(final Parcel parcel) { 934 | return new CarouselSavedState(parcel); 935 | } 936 | 937 | @Override 938 | public CarouselSavedState[] newArray(final int i) { 939 | return new CarouselSavedState[i]; 940 | } 941 | }; 942 | } 943 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/src/main/java/com/mig35/carousellayoutmanager/CarouselSmoothScroller.java: -------------------------------------------------------------------------------- 1 | package com.mig35.carousellayoutmanager; 2 | 3 | import android.graphics.PointF; 4 | import androidx.annotation.NonNull; 5 | import androidx.recyclerview.widget.RecyclerView; 6 | import android.view.View; 7 | 8 | /** 9 | * Custom implementation of {@link RecyclerView.SmoothScroller} that can work only with {@link CarouselLayoutManager}. 10 | * 11 | * @see CarouselLayoutManager 12 | */ 13 | public class CarouselSmoothScroller { 14 | 15 | public CarouselSmoothScroller(@NonNull final RecyclerView.State state, final int position) { 16 | if (0 > position) { 17 | throw new IllegalArgumentException("position can't be less then 0. position is : " + position); 18 | } 19 | if (position >= state.getItemCount()) { 20 | throw new IllegalArgumentException("position can't be great then adapter items count. position is : " + position); 21 | } 22 | } 23 | 24 | @SuppressWarnings("unused") 25 | public PointF computeScrollVectorForPosition(final int targetPosition, @NonNull final CarouselLayoutManager carouselLayoutManager) { 26 | return carouselLayoutManager.computeScrollVectorForPosition(targetPosition); 27 | } 28 | 29 | @SuppressWarnings("unused") 30 | public int calculateDyToMakeVisible(final View view, @NonNull final CarouselLayoutManager carouselLayoutManager) { 31 | if (!carouselLayoutManager.canScrollVertically()) { 32 | return 0; 33 | } 34 | 35 | return carouselLayoutManager.getOffsetForCurrentView(view); 36 | } 37 | 38 | @SuppressWarnings("unused") 39 | public int calculateDxToMakeVisible(final View view, @NonNull final CarouselLayoutManager carouselLayoutManager) { 40 | if (!carouselLayoutManager.canScrollHorizontally()) { 41 | return 0; 42 | } 43 | return carouselLayoutManager.getOffsetForCurrentView(view); 44 | } 45 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/src/main/java/com/mig35/carousellayoutmanager/CarouselZoomPostLayoutListener.java: -------------------------------------------------------------------------------- 1 | package com.mig35.carousellayoutmanager; 2 | 3 | import androidx.annotation.NonNull; 4 | import android.view.View; 5 | 6 | /** 7 | * Implementation of {@link CarouselLayoutManager.PostLayoutListener} that makes interesting scaling of items.
8 | * We are trying to make items scaling quicker for closer items for center and slower for when they are far away.
9 | * Tis implementation uses atan function for this purpose. 10 | */ 11 | public class CarouselZoomPostLayoutListener extends CarouselLayoutManager.PostLayoutListener { 12 | 13 | private final float mScaleMultiplier; 14 | 15 | public CarouselZoomPostLayoutListener() { 16 | this(0.17f); 17 | } 18 | 19 | public CarouselZoomPostLayoutListener(final float scaleMultiplier) { 20 | mScaleMultiplier = scaleMultiplier; 21 | } 22 | 23 | @Override 24 | public ItemTransformation transformChild(@NonNull final View child, final float itemPositionToCenterDiff, final int orientation) { 25 | final float scale = 1.0f - mScaleMultiplier * Math.abs(itemPositionToCenterDiff); 26 | 27 | // because scaling will make view smaller in its center, then we should move this item to the top or bottom to make it visible 28 | final float translateY; 29 | final float translateX; 30 | if (CarouselLayoutManager.VERTICAL == orientation) { 31 | final float translateYGeneral = child.getMeasuredHeight() * (1 - scale) / 2f; 32 | translateY = Math.signum(itemPositionToCenterDiff) * translateYGeneral; 33 | translateX = 0; 34 | } else { 35 | final float translateXGeneral = child.getMeasuredWidth() * (1 - scale) / 2f; 36 | translateX = Math.signum(itemPositionToCenterDiff) * translateXGeneral; 37 | translateY = 0; 38 | } 39 | 40 | return new ItemTransformation(scale, scale, translateX, translateY); 41 | } 42 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/src/main/java/com/mig35/carousellayoutmanager/CenterScrollListener.java: -------------------------------------------------------------------------------- 1 | package com.mig35.carousellayoutmanager; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.recyclerview.widget.RecyclerView; 5 | 6 | /** 7 | * Class for centering items after scroll event.
8 | * This class will listen to current scroll state and if item is not centered after scroll it will automatically scroll it to center. 9 | */ 10 | public class CenterScrollListener extends RecyclerView.OnScrollListener { 11 | 12 | private boolean mAutoSet = true; 13 | 14 | @Override 15 | public void onScrollStateChanged(@NonNull final RecyclerView recyclerView, final int newState) { 16 | super.onScrollStateChanged(recyclerView, newState); 17 | final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 18 | if (!(layoutManager instanceof CarouselLayoutManager)) { 19 | mAutoSet = true; 20 | return; 21 | } 22 | 23 | final CarouselLayoutManager lm = (CarouselLayoutManager) layoutManager; 24 | if (!mAutoSet) { 25 | if (RecyclerView.SCROLL_STATE_IDLE == newState) { 26 | final int scrollNeeded = lm.getOffsetCenterView(); 27 | if (CarouselLayoutManager.HORIZONTAL == lm.getOrientation()) { 28 | recyclerView.smoothScrollBy(scrollNeeded, 0); 29 | } else { 30 | recyclerView.smoothScrollBy(0, scrollNeeded); 31 | } 32 | mAutoSet = true; 33 | } 34 | } 35 | if (RecyclerView.SCROLL_STATE_DRAGGING == newState || RecyclerView.SCROLL_STATE_SETTLING == newState) { 36 | mAutoSet = false; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/src/main/java/com/mig35/carousellayoutmanager/DefaultChildSelectionListener.java: -------------------------------------------------------------------------------- 1 | package com.mig35.carousellayoutmanager; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.recyclerview.widget.RecyclerView; 5 | import android.view.View; 6 | 7 | public class DefaultChildSelectionListener extends CarouselChildSelectionListener { 8 | 9 | @NonNull 10 | private final OnCenterItemClickListener mOnCenterItemClickListener; 11 | 12 | protected DefaultChildSelectionListener(@NonNull final OnCenterItemClickListener onCenterItemClickListener, @NonNull final RecyclerView recyclerView, @NonNull final CarouselLayoutManager carouselLayoutManager) { 13 | super(recyclerView, carouselLayoutManager); 14 | 15 | mOnCenterItemClickListener = onCenterItemClickListener; 16 | } 17 | 18 | @Override 19 | protected void onCenterItemClicked(@NonNull final RecyclerView recyclerView, @NonNull final CarouselLayoutManager carouselLayoutManager, @NonNull final View v) { 20 | mOnCenterItemClickListener.onCenterItemClicked(recyclerView, carouselLayoutManager, v); 21 | } 22 | 23 | @Override 24 | protected void onBackItemClicked(@NonNull final RecyclerView recyclerView, @NonNull final CarouselLayoutManager carouselLayoutManager, @NonNull final View v) { 25 | recyclerView.smoothScrollToPosition(carouselLayoutManager.getPosition(v)); 26 | } 27 | 28 | public static DefaultChildSelectionListener initCenterItemListener(@NonNull final OnCenterItemClickListener onCenterItemClickListener, @NonNull final RecyclerView recyclerView, @NonNull final CarouselLayoutManager carouselLayoutManager) { 29 | return new DefaultChildSelectionListener(onCenterItemClickListener, recyclerView, carouselLayoutManager); 30 | } 31 | 32 | public interface OnCenterItemClickListener { 33 | 34 | void onCenterItemClicked(@NonNull final RecyclerView recyclerView, @NonNull final CarouselLayoutManager carouselLayoutManager, @NonNull final View v); 35 | } 36 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/carousellayoutmanager/src/main/java/com/mig35/carousellayoutmanager/ItemTransformation.java: -------------------------------------------------------------------------------- 1 | package com.mig35.carousellayoutmanager; 2 | 3 | public class ItemTransformation { 4 | 5 | final float mScaleX; 6 | final float mScaleY; 7 | final float mTranslationX; 8 | final float mTranslationY; 9 | 10 | public ItemTransformation(final float scaleX, final float scaleY, final float translationX, final float translationY) { 11 | mScaleX = scaleX; 12 | mScaleY = scaleY; 13 | mTranslationX = translationX; 14 | mTranslationY = translationY; 15 | } 16 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | android.enableJetifier=true 14 | android.useAndroidX=true 15 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 16 | 17 | # When configured, Gradle will run in incubating parallel mode. 18 | # This option should only be used with decoupled projects. More details, visit 19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 20 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /CarouselLayoutManager/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azoft/CarouselLayoutManager/979b5d3c17f44c5b5c1fa57472a8163efb2e9075/CarouselLayoutManager/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /CarouselLayoutManager/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jan 18 09:06:02 NOVT 2021 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.7.1-bin.zip 7 | -------------------------------------------------------------------------------- /CarouselLayoutManager/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /CarouselLayoutManager/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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdkVersion 30 7 | 8 | defaultConfig { 9 | applicationId "com.mig35.carousellayoutmanager.sample" 10 | minSdkVersion 14 11 | targetSdkVersion 30 12 | versionCode 1 13 | versionName "1.0" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | buildFeatures { 22 | viewBinding true 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(include: ['*.jar'], dir: 'libs') 28 | 29 | implementation 'androidx.appcompat:appcompat:1.2.0' 30 | implementation 'com.google.android.material:material:1.3.0' 31 | implementation 'androidx.recyclerview:recyclerview:1.2.0' 32 | implementation 'com.mig35:carousellayoutmanager:+' 33 | // implementation project(':carousellayoutmanager') 34 | } 35 | -------------------------------------------------------------------------------- /CarouselLayoutManager/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\Mikhail\Documents\tools\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 | -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/java/com/mig35/carousellayoutmanager/sample/CarouselPreviewActivity.java: -------------------------------------------------------------------------------- 1 | package com.mig35.carousellayoutmanager.sample; 2 | 3 | import android.graphics.Color; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.Toast; 10 | 11 | import com.mig35.carousellayoutmanager.CarouselLayoutManager; 12 | import com.mig35.carousellayoutmanager.CarouselZoomPostLayoutListener; 13 | import com.mig35.carousellayoutmanager.CenterScrollListener; 14 | import com.mig35.carousellayoutmanager.DefaultChildSelectionListener; 15 | import com.mig35.carousellayoutmanager.sample.databinding.ActivityCarouselPreviewBinding; 16 | import com.mig35.carousellayoutmanager.sample.databinding.ItemViewBinding; 17 | 18 | import java.util.Locale; 19 | import java.util.Random; 20 | 21 | import androidx.annotation.NonNull; 22 | import androidx.appcompat.app.AppCompatActivity; 23 | import androidx.recyclerview.widget.RecyclerView; 24 | 25 | public class CarouselPreviewActivity extends AppCompatActivity { 26 | 27 | @Override 28 | protected void onCreate(final Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | 31 | final ActivityCarouselPreviewBinding binding = ActivityCarouselPreviewBinding.inflate(getLayoutInflater()); 32 | setContentView(binding.getRoot()); 33 | 34 | setSupportActionBar(binding.toolbar); 35 | 36 | final TestAdapter adapter = new TestAdapter(); 37 | 38 | // create layout manager with needed params: vertical, cycle 39 | initRecyclerView(binding.listHorizontal, new CarouselLayoutManager(CarouselLayoutManager.HORIZONTAL, false), adapter); 40 | initRecyclerView(binding.listVertical, new CarouselLayoutManager(CarouselLayoutManager.VERTICAL, true), adapter); 41 | 42 | // fab button will add element to the end of the list 43 | binding.fabScroll.setOnClickListener(new View.OnClickListener() { 44 | @Override 45 | public void onClick(final View v) { 46 | /* 47 | final int itemToRemove = adapter.mItemsCount; 48 | if (10 != itemToRemove) { 49 | adapter.mItemsCount++; 50 | adapter.notifyItemInserted(itemToRemove); 51 | } 52 | */ 53 | binding.listHorizontal.smoothScrollToPosition(adapter.getItemCount() - 2); 54 | binding.listVertical.smoothScrollToPosition(adapter.getItemCount() - 2); 55 | } 56 | }); 57 | 58 | // fab button will remove element from the end of the list 59 | binding.fabChangeData.setOnClickListener(new View.OnClickListener() { 60 | @Override 61 | public void onClick(final View v) { 62 | /* 63 | final int itemToRemove = adapter.mItemsCount - 1; 64 | if (0 <= itemToRemove) { 65 | adapter.mItemsCount--; 66 | adapter.notifyItemRemoved(itemToRemove); 67 | } 68 | */ 69 | binding.listHorizontal.smoothScrollToPosition(1); 70 | binding.listVertical.smoothScrollToPosition(1); 71 | } 72 | }); 73 | } 74 | 75 | private void initRecyclerView(final RecyclerView recyclerView, final CarouselLayoutManager layoutManager, final TestAdapter adapter) { 76 | // enable zoom effect. this line can be customized 77 | layoutManager.setPostLayoutListener(new CarouselZoomPostLayoutListener()); 78 | layoutManager.setMaxVisibleItems(3); 79 | 80 | recyclerView.setLayoutManager(layoutManager); 81 | // we expect only fixed sized item for now 82 | recyclerView.setHasFixedSize(true); 83 | // sample adapter with random data 84 | recyclerView.setAdapter(adapter); 85 | // enable center post scrolling 86 | recyclerView.addOnScrollListener(new CenterScrollListener()); 87 | // enable center post touching on item and item click listener 88 | DefaultChildSelectionListener.initCenterItemListener(new DefaultChildSelectionListener.OnCenterItemClickListener() { 89 | @Override 90 | public void onCenterItemClicked(@NonNull final RecyclerView recyclerView, @NonNull final CarouselLayoutManager carouselLayoutManager, @NonNull final View v) { 91 | final int position = recyclerView.getChildLayoutPosition(v); 92 | final String msg = String.format(Locale.US, "Item %1$d was clicked", position); 93 | Toast.makeText(CarouselPreviewActivity.this, msg, Toast.LENGTH_SHORT).show(); 94 | } 95 | }, recyclerView, layoutManager); 96 | 97 | layoutManager.addOnItemSelectionListener(new CarouselLayoutManager.OnCenterItemSelectionListener() { 98 | 99 | @Override 100 | public void onCenterItemChanged(final int adapterPosition) { 101 | if (CarouselLayoutManager.INVALID_POSITION != adapterPosition) { 102 | final int value = adapter.mPosition[adapterPosition]; 103 | /* 104 | adapter.mPosition[adapterPosition] = (value % 10) + (value / 10 + 1) * 10; 105 | adapter.notifyItemChanged(adapterPosition); 106 | */ 107 | } 108 | } 109 | }); 110 | } 111 | 112 | private static final class TestAdapter extends RecyclerView.Adapter { 113 | 114 | private final int[] mColors; 115 | private final int[] mPosition; 116 | private final int mItemsCount = 100; 117 | 118 | TestAdapter() { 119 | mColors = new int[mItemsCount]; 120 | mPosition = new int[mItemsCount]; 121 | for (int i = 0; mItemsCount > i; ++i) { 122 | //noinspection MagicNumber 123 | final Random random = new Random(); 124 | mColors[i] = Color.argb(255, random.nextInt(256), random.nextInt(256), random.nextInt(256)); 125 | mPosition[i] = i; 126 | } 127 | } 128 | 129 | @Override 130 | public TestViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { 131 | Log.e("!!!!!!!!!", "onCreateViewHolder"); 132 | return new TestViewHolder(ItemViewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); 133 | } 134 | 135 | @Override 136 | public void onBindViewHolder(final TestViewHolder holder, final int position) { 137 | Log.e("!!!!!!!!!", "onBindViewHolder: " + position); 138 | holder.mItemViewBinding.cItem1.setText(String.valueOf(mPosition[position])); 139 | holder.mItemViewBinding.cItem2.setText(String.valueOf(mPosition[position])); 140 | holder.itemView.setBackgroundColor(mColors[position]); 141 | } 142 | 143 | @Override 144 | public int getItemCount() { 145 | return mItemsCount; 146 | } 147 | 148 | @Override 149 | public long getItemId(final int position) { 150 | return position; 151 | } 152 | } 153 | 154 | private static class TestViewHolder extends RecyclerView.ViewHolder { 155 | 156 | private final ItemViewBinding mItemViewBinding; 157 | 158 | TestViewHolder(final ItemViewBinding itemViewBinding) { 159 | super(itemViewBinding.getRoot()); 160 | 161 | mItemViewBinding = itemViewBinding; 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/layout/activity_carousel_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 30 | 31 | 38 | 39 | 44 | 45 | 50 | 51 | 52 | 53 | 60 | 61 | 66 | 67 | 72 | 73 | 74 | 75 | 76 | 77 | 85 | 86 | 94 | 95 | -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/layout/item_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 13 | 14 | 20 | 21 | 26 | 27 | 32 | 33 | -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azoft/CarouselLayoutManager/979b5d3c17f44c5b5c1fa57472a8163efb2e9075/CarouselLayoutManager/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azoft/CarouselLayoutManager/979b5d3c17f44c5b5c1fa57472a8163efb2e9075/CarouselLayoutManager/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azoft/CarouselLayoutManager/979b5d3c17f44c5b5c1fa57472a8163efb2e9075/CarouselLayoutManager/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azoft/CarouselLayoutManager/979b5d3c17f44c5b5c1fa57472a8163efb2e9075/CarouselLayoutManager/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azoft/CarouselLayoutManager/979b5d3c17f44c5b5c1fa57472a8163efb2e9075/CarouselLayoutManager/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CarouselLayoutManager 3 | 4 | -------------------------------------------------------------------------------- /CarouselLayoutManager/sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |