├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── README.md ├── Showcase ├── bg.png ├── cover_static.png ├── ffmpeg_commands ├── foreground.png ├── gifs │ ├── 3d_transform.webp │ ├── capped_linear_interpolator.webp │ ├── default_interpolator.webp │ ├── dense_interpolator.webp │ ├── linear_interpolator.webp │ ├── log_decelerate_60_interpolator.webp │ ├── nice_combo_capped_scale_out.webp │ ├── nice_combo_linear_3d.webp │ ├── nice_combo_linear_scale_both.webp │ ├── nice_combo_shazam.webp │ ├── overshooting_interpolator.webp │ ├── reverse_interpolator.webp │ ├── scale_in_out.webp │ ├── scale_in_transform.webp │ └── scale_out_transform.webp ├── logo.ai ├── overlay.ai └── screenshot_demo_app.png ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output-metadata.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── net │ │ └── darkion │ │ └── stacklayoutmanager │ │ └── demo │ │ ├── Card.kt │ │ ├── Data.kt │ │ ├── DoubleTextView.kt │ │ └── MainActivity.kt │ └── res │ ├── drawable │ ├── card_bg.xml │ ├── circle_bg.xml │ ├── header_bg.xml │ ├── ic_arrows.xml │ ├── ic_baseline_aspect_ratio_24.xml │ ├── ic_baseline_crop_square_24.xml │ ├── ic_bullseye_gradient.xml │ ├── ic_confetti_doodles.xml │ ├── ic_cornered_stairs.xml │ ├── ic_curve_www_wishforge_games.xml │ ├── ic_eye.xml │ ├── ic_flat_mountains.xml │ ├── ic_geometric_intersection.xml │ ├── ic_hollowed_boxes.xml │ ├── ic_magnet.xml │ ├── ic_rainbow_vortex.xml │ ├── ic_repeating_chevrons.xml │ └── ic_scale_royyan_wijaya.xml │ ├── font │ ├── poppins_medium.ttf │ └── share_tech_mono.ttf │ ├── layout │ ├── activity_main.xml │ └── recycler_view_item.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_background.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── stacklayoutmanager-extras ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── library │ └── stacklayoutmanager │ └── extras │ ├── layoutinterpolators │ ├── CappedLinearInterpolator.kt │ ├── DenseStackInterpolator.kt │ ├── FreePathInterpolator.java │ ├── LogDecelerateInterpolator.java │ ├── OvershootingInterpolator.kt │ └── ReverseStackInterpolator.kt │ └── transformers │ ├── RotationTransformer.kt │ ├── ScaleInOnlyTransformer.kt │ ├── ScaleOutOnlyTransformer.kt │ └── ScaleTransformer.kt └── stacklayoutmanager ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml └── java └── library └── StackLayoutManager.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /app/release/ 17 | /gradle/ 18 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | 6 | 7 | # StackLayoutManager 8 | StackLayoutManager is a lightweight and highly-customizable layout manager for RecyclerView which lays out views by using a [`TimeInterpolator`](https://developer.android.com/reference/android/animation/TimeInterpolator) object. This makes the views travel along the path that the interpolator provides, thus allowing us to create sophisticated transitions. Additionally, it supports setting a custom view transformation callback, which allows more elaborate transitions to be made through direct access to all transformation properties a view can have. 9 | 10 | ![rep size](https://img.shields.io/github/repo-size/DarkionAvey/StackLayoutManager) 11 | 12 | # Demo app 13 | You can download the demo APK from [here](https://github.com/DarkionAvey/StackLayoutManager/releases/download/v1/app-release.apk) 14 | 15 | # Installation 16 | StackLayoutManager is a single Kotlin file, so you just need to copy the [`StackLayoutManager.kt`](https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/stacklayoutmanager/src/main/java/library/StackLayoutManager.kt) file to your project and change the `package` declaration. That's it! 17 | 18 |
19 | 20 | # Showcase 21 | 22 | ## Interpolators 23 | 24 | |![Default/LogDecelerateInterpolator](https://github.com/DarkionAvey/StackLayoutManager/blob/master/Showcase/gifs/log_decelerate_60_interpolator.webp?raw=true)Default|![DenseStackInterpolator](https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/dense_interpolator.webp)[`DenseStackInterpolator.kt`](https://github.com/DarkionAvey/StackLayoutManager/blob/master/app/src/main/java/net/darkion/stacklayoutmanager/demo/layoutinterpolators/DenseStackInterpolator.kt)|![LinearInterpolator](https://github.com/DarkionAvey/StackLayoutManager/blob/master/Showcase/gifs/linear_interpolator.webp?raw=true)`LinearInterpolator`| 25 | | :--: | :--: | :--: | 26 | |![OvershootingInterpolator](https://github.com/DarkionAvey/StackLayoutManager/blob/master/Showcase/gifs/overshooting_interpolator.webp?raw=true)[`OvershootingInterpolator.kt`](https://github.com/DarkionAvey/StackLayoutManager/blob/master/stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/layoutinterpolators/OvershootingInterpolator.kt)|![ReverseStackInterpolator](https://github.com/DarkionAvey/StackLayoutManager/blob/master/Showcase/gifs/reverse_interpolator.webp?raw=true)[`ReverseStackInterpolator.kt`](https://github.com/DarkionAvey/StackLayoutManager/blob/master/stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/layoutinterpolators/ReverseStackInterpolator.kt) |![CappedLinearInterpolator](https://github.com/DarkionAvey/StackLayoutManager/blob/master/Showcase/gifs/capped_linear_interpolator.webp?raw=true)[`CappedLinearInterpolator.kt`](https://github.com/DarkionAvey/StackLayoutManager/blob/master/stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/layoutinterpolators/CappedLinearInterpolator.kt)| 27 | 28 | 29 | ## Transformers 30 | |![ScaleInOnlyTransformer](https://github.com/DarkionAvey/StackLayoutManager/blob/master/Showcase/gifs/scale_in_transform.webp?raw=true)|![ScaleOutOnlyTransformer](https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/scale_out_transform.webp)|![ScaleTransformer](https://github.com/DarkionAvey/StackLayoutManager/blob/master/Showcase/gifs/scale_in_out.webp?raw=true)|![RotationTransformer](https://github.com/DarkionAvey/StackLayoutManager/blob/master/Showcase/gifs/3d_transform.webp?raw=true)| 31 | | :--: | :--: | :--: | :--: | 32 | |[`ScaleIn...former.kt`]https://github.com/DarkionAvey/StackLayoutManager/blob/master/stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/transformers/ScaleInOnlyTransformer.kt)|[`ScaleOut...ormer.kt`](https://github.com/DarkionAvey/StackLayoutManager/blob/master/stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/transformers/ScaleOutOnlyTransformer.kt)|[`ScaleTransformer.kt`](https://github.com/DarkionAvey/StackLayoutManager/blob/master/stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/transformers/ScaleTransformer.kt)|[`Rotation...ormer.kt`](https://github.com/DarkionAvey/StackLayoutManager/blob/master/stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/transformers/RotationTransformer.kt)| 33 | 34 | # Setup 35 | StackLayoutManager constructor doesn't require any parameters, so you can just use it directly like this 36 | ``` 37 | recyclerView.layoutManager = StackLayoutManager() 38 | ``` 39 | By default, StackLayoutManager will layout views horizontally and will center the first item (`position == 0`). If you want to change either of those, or change other parameters, the constructor has a few optional, named parameters which you can set to customize your experience. Those parameters are: 40 | 41 | 42 | | Parameter | Type | Default | Description | 43 | |:---: | :-: | :-: | :--- | 44 | | `horizontalLayout` | Boolean | `true` | whether views should be laid out horizontally or vertically| 45 | | `centerFirstItem` | Boolean | `true` | whether the first item (with `position == 0`) should be centered. In some instances, specially when using Log interpolators, setting this to false will position the first child off-screen which could be confusing to users | 46 | | `scrollMultiplier` | Float | `1.2f` | the number of items (as float, where each item is equal to `1f`) that should be scrolled in one swipe. Setting higher values will cause stuttering because of float rounding. It is recommended that you set this to `1.2f` | 47 | | `maxViews` | Int | `6` | the maximum number of views that the RecyclerView should have. Higher values require more resources and might cause lag in some cases. It may be necessary to increase this number when using layout interpolators that create stacking effects, such as the case with [`DenseStackInterpolator.kt`](https://github.com/DarkionAvey/StackLayoutManager/blob/master/app/src/main/java/net/darkion/stacklayoutmanager/demo/layoutinterpolators/DenseStackInterpolator.kt). It is recommended that you set this to anything between 6 and 10 | 48 | | `layoutInterpolator` | TimeInterpolator | `LogDecelerateInterpolator` | the interpolator which will be used to layout views. Any type of interpolator is accepted, such as `LinearInterpolator()`, `FastOutSlowInInterpolator()`, or the ones shown in the demo app. The interpolation typically starts from `x, y = 0f, 0f` and ends at `x, y = 1f, 1f`, but that is not always the case, as demonstrated in [`DenseStackInterpolator.kt`](https://github.com/DarkionAvey/StackLayoutManager/blob/master/app/src/main/java/net/darkion/stacklayoutmanager/demo/layoutinterpolators/DenseStackInterpolator.kt). StackLayoutManager itself comes with only one interpolator, so you might need to copy one of the provided interpolators from the demo app or make your own interpolator. Check out the showcase section to see some of the provided interpolators | 49 | | `viewTransformer` | `((x: Float, view: View, stackLayoutManager: StackLayoutManager) -> Unit?)?` | [`ElevationTransformer`](https://github.com/DarkionAvey/StackLayoutManager/blob/fc5f05a72d4d9a784783cda1c2403ef8ccc7175b/stacklayoutmanager/src/main/java/net/darkion/stacklayoutmanager/library/StackLayoutManager.kt#L449) | supply a higher-order function that takes `Float, View, StackLayoutManager` and returns `Unit` (void). This is used to set a callback which allows the caller to have direct access to a view after it has been laid out by the layout manager. By using this, you will be able to apply additional transforms to the view, including the stock transforms such as **rotation**, **scale**, **elevation**; as well as any custom attribute of a custom view. The descriptions of the function parameters are as follows: Check out the showcase section to see some of the provided transformers| 50 | 51 | # Public functions 52 | | Function | Description | 53 | |:--- | :--- | 54 | | `scrollToPosition` | scroll to position with more control over the animation compared to the method provided by the default layout manager. Using this function, you can pass a float value as the target position, which translates to "target position + extra distance". You can also pass named parameters for more control. Those include: `animated`; `duration` which is applicable when `animated` is `true`; and `endRunnable` which is executed after the scroll has ended| 55 | | `forceRemeasureInNextLayout` | notify the layout manager that the dimensions of the views have changed. By default, the layout manager measures only one child and then assumes the same dimensions for the rest of the views. Calling this method will remeasure that one standard view | 56 | | `findFirstVisibleItemPosition`, `findFirstCompletelyVisibleItemPosition`, `findLastVisibleItemPosition`, and `findLastCompletelyVisibleItemPosition` | return the corresponding position of view as described in the function's name | 57 | | `stopScrolling` | helper method to force stop scroll animator. This does not stop the scroll initiated by user input (e.g., fling) | 58 | | `peek` | a helpful function that can be used to nudge or guide the user to the correct scrolling direction, or to inform the user that there are some views that are off-screen | 59 | | `snap` | as the name suggests, this will cause the layout manager to snap to the next or previous view depending on the current scroll fraction | 60 | 61 | # Credits 62 | The demo app uses assets from: 63 | * [SVGBackgrounds](https://www.svgbackgrounds.com/) 64 | * [Simone Hutsch @ Unsplash](https://unsplash.com/@heysupersimi) 65 | * WishforgeGames @ IconFinder 66 | * RoyyanWijaya @ IconFinder 67 | 68 | # License 69 | MIT 70 | ``` 71 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /Showcase/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/bg.png -------------------------------------------------------------------------------- /Showcase/cover_static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/cover_static.png -------------------------------------------------------------------------------- /Showcase/ffmpeg_commands: -------------------------------------------------------------------------------- 1 | ffmpeg -i input.mp4 -ss 00:00:03 -t 00:00:06.13 trimmed.mp4 2 | 3 | ffmpeg -i trimmed.mp4 -filter:v "crop=1080:1280:0:630" cropped.mp4 4 | 5 | ffmpeg -i cropped.mp4 -vf "pad=width=2200:height=1280:x=1120:y=0:color=#ffffff" padded.mp4 6 | 7 | ffmpeg -i padded.mp4 -i overlay.png -filter_complex "overlay=0:0" cover.mp4 8 | 9 | ffmpeg -i input.mp4 -vcodec libwebp -lossless 1 -loop 0 -preset default output.webp 10 | -------------------------------------------------------------------------------- /Showcase/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/foreground.png -------------------------------------------------------------------------------- /Showcase/gifs/3d_transform.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/3d_transform.webp -------------------------------------------------------------------------------- /Showcase/gifs/capped_linear_interpolator.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/capped_linear_interpolator.webp -------------------------------------------------------------------------------- /Showcase/gifs/default_interpolator.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/default_interpolator.webp -------------------------------------------------------------------------------- /Showcase/gifs/dense_interpolator.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/dense_interpolator.webp -------------------------------------------------------------------------------- /Showcase/gifs/linear_interpolator.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/linear_interpolator.webp -------------------------------------------------------------------------------- /Showcase/gifs/log_decelerate_60_interpolator.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/log_decelerate_60_interpolator.webp -------------------------------------------------------------------------------- /Showcase/gifs/nice_combo_capped_scale_out.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/nice_combo_capped_scale_out.webp -------------------------------------------------------------------------------- /Showcase/gifs/nice_combo_linear_3d.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/nice_combo_linear_3d.webp -------------------------------------------------------------------------------- /Showcase/gifs/nice_combo_linear_scale_both.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/nice_combo_linear_scale_both.webp -------------------------------------------------------------------------------- /Showcase/gifs/nice_combo_shazam.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/nice_combo_shazam.webp -------------------------------------------------------------------------------- /Showcase/gifs/overshooting_interpolator.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/overshooting_interpolator.webp -------------------------------------------------------------------------------- /Showcase/gifs/reverse_interpolator.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/reverse_interpolator.webp -------------------------------------------------------------------------------- /Showcase/gifs/scale_in_out.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/scale_in_out.webp -------------------------------------------------------------------------------- /Showcase/gifs/scale_in_transform.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/scale_in_transform.webp -------------------------------------------------------------------------------- /Showcase/gifs/scale_out_transform.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/gifs/scale_out_transform.webp -------------------------------------------------------------------------------- /Showcase/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/logo.ai -------------------------------------------------------------------------------- /Showcase/overlay.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/overlay.ai -------------------------------------------------------------------------------- /Showcase/screenshot_demo_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/Showcase/screenshot_demo_app.png -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release/ 3 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | applicationId "net.darkion.stacklayoutmanager.demo" 12 | minSdkVersion 21 13 | targetSdkVersion 30 14 | versionCode 1 15 | versionName "1.0" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | kotlinOptions { 29 | jvmTarget = '1.8' 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation 'com.github.bumptech.glide:glide:4.12.0' 35 | annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 37 | implementation 'androidx.core:core-ktx:1.6.0' 38 | implementation 'androidx.appcompat:appcompat:1.3.1' 39 | implementation 'com.google.android.material:material:1.4.0' 40 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0' 41 | implementation project(path: ':stacklayoutmanager') 42 | implementation project(path: ':stacklayoutmanager-extras') 43 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "net.darkion.stacklayoutmanager.demo", 8 | "variantName": "processReleaseResources", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "versionCode": 1, 14 | "versionName": "1.0", 15 | "outputFile": "app-release.apk" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/net/darkion/stacklayoutmanager/demo/Card.kt: -------------------------------------------------------------------------------- 1 | package net.darkion.stacklayoutmanager.demo 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.util.AttributeSet 7 | import android.view.ViewOutlineProvider 8 | import androidx.constraintlayout.widget.ConstraintLayout 9 | import androidx.core.graphics.ColorUtils 10 | import library.StackLayoutManager 11 | 12 | 13 | //this view demonstrates how to use the 14 | //dimming value supplied by StackLayoutParams 15 | //(see 'override fun draw' method) 16 | class Card @JvmOverloads constructor( 17 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 18 | ) : ConstraintLayout(context, attrs, defStyleAttr) { 19 | 20 | init { 21 | outlineProvider = ViewOutlineProvider.BACKGROUND 22 | clipToOutline = true 23 | } 24 | 25 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 26 | val smallerSpec: Int? = if (MainActivity.squareItems) { 27 | val width = MeasureSpec.getSize(widthMeasureSpec) 28 | val height = MeasureSpec.getSize(heightMeasureSpec) 29 | if (width < height) widthMeasureSpec else heightMeasureSpec 30 | } else null 31 | super.onMeasure( 32 | smallerSpec ?: widthMeasureSpec, 33 | smallerSpec ?: heightMeasureSpec 34 | ) 35 | } 36 | 37 | //we use draw instead of onDraw to 38 | //draw over the image view which is a 39 | //child of this ViewGroup 40 | override fun draw(canvas: Canvas) { 41 | super.draw(canvas) 42 | 43 | canvas.drawColor( 44 | ColorUtils.setAlphaComponent( 45 | Color.BLACK, 46 | ((layoutParams as StackLayoutManager.StackLayoutParams).dimAmount * 255).toInt() 47 | ) 48 | ) 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/net/darkion/stacklayoutmanager/demo/Data.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("ConstantConditionIf") 2 | 3 | package net.darkion.stacklayoutmanager.demo 4 | 5 | //if not connected to internet, show the 6 | //drawables that are packaged with the apk 7 | class Data(connected: Boolean) { 8 | 9 | //names of the transformers for displaying toasts 10 | val transformersNames by lazy { 11 | arrayOf( 12 | "Default (ElevationTransformer)", 13 | "Scale Out", 14 | "Scale In", 15 | "Scale In and Out ", 16 | "3D rotation" 17 | ) 18 | } 19 | 20 | val images: Array = 21 | if (connected || MainActivity.showcaseMode) { 22 | arrayOf( 23 | //amazing architecture pictures by Simone Hutsch 24 | "https://images.unsplash.com/photo-1506438689880-92e5d6b50ff9?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80" as Any, 25 | "https://images.unsplash.com/photo-1532079746053-4fb0c408ce6e?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=2040&q=80", 26 | "https://images.unsplash.com/photo-1534240724593-cfacb25cf6ad?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=635&q=80", 27 | "https://images.unsplash.com/photo-1536736693558-ad17e44c156b?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1378&q=80", 28 | "https://images.unsplash.com/photo-1603347201544-a4fba96efa04?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=675&q=80", 29 | "https://images.unsplash.com/photo-1531319596683-4712e6d1db2c?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1943&q=80", 30 | "https://images.unsplash.com/photo-1535462009050-27ddea306055?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=649&q=80", 31 | "https://images.unsplash.com/photo-1529307474719-3d0a417aaf8a?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=627&q=80", 32 | "https://images.unsplash.com/photo-1526547050953-b9fe7299eb69?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=657&q=80", 33 | "https://images.unsplash.com/photo-1531323803217-ba21570d0e41?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=2128&q=80" 34 | ) 35 | } else { 36 | arrayOf( 37 | R.drawable.ic_bullseye_gradient as Any, 38 | R.drawable.ic_flat_mountains, 39 | R.drawable.ic_confetti_doodles, 40 | R.drawable.ic_rainbow_vortex, 41 | R.drawable.ic_repeating_chevrons, 42 | R.drawable.ic_hollowed_boxes, 43 | R.drawable.ic_geometric_intersection, 44 | R.drawable.ic_cornered_stairs 45 | ) 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/net/darkion/stacklayoutmanager/demo/DoubleTextView.kt: -------------------------------------------------------------------------------- 1 | package net.darkion.stacklayoutmanager.demo 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.graphics.Canvas 6 | import android.graphics.Color 7 | import android.graphics.Paint 8 | import android.util.AttributeSet 9 | import android.util.TypedValue 10 | import androidx.appcompat.widget.AppCompatTextView 11 | import androidx.core.graphics.ColorUtils 12 | 13 | class DoubleTextView @JvmOverloads constructor( 14 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 15 | ) : AppCompatTextView(context, attrs, defStyleAttr) { 16 | private val strokeWidth = TypedValue.applyDimension( 17 | TypedValue.COMPLEX_UNIT_DIP, 18 | 10f, 19 | Resources.getSystem().displayMetrics 20 | ) 21 | 22 | override fun onDraw(canvas: Canvas?) { 23 | paint.style = Paint.Style.FILL_AND_STROKE 24 | paint.strokeWidth = strokeWidth 25 | setTextColor(ColorUtils.setAlphaComponent(Color.BLACK, 80)) 26 | super.onDraw(canvas) 27 | 28 | paint.style = Paint.Style.FILL 29 | paint.strokeWidth = 0f 30 | setTextColor(Color.WHITE) 31 | super.onDraw(canvas) 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/net/darkion/stacklayoutmanager/demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package net.darkion.stacklayoutmanager.demo 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.net.ConnectivityManager 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.view.animation.LinearInterpolator 13 | import android.widget.TextView 14 | import androidx.appcompat.app.AppCompatActivity 15 | import androidx.appcompat.widget.AppCompatImageView 16 | import androidx.core.view.children 17 | import androidx.recyclerview.widget.RecyclerView 18 | import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING 19 | import com.bumptech.glide.Glide 20 | import com.bumptech.glide.load.DecodeFormat 21 | import com.google.android.material.snackbar.Snackbar 22 | import library.StackLayoutManager 23 | import library.stacklayoutmanager.extras.layoutinterpolators.* 24 | import library.stacklayoutmanager.extras.transformers.RotationTransformer 25 | import library.stacklayoutmanager.extras.transformers.ScaleInOnlyTransformer 26 | import library.stacklayoutmanager.extras.transformers.ScaleOutOnlyTransformer 27 | import library.stacklayoutmanager.extras.transformers.ScaleTransformer 28 | 29 | 30 | @Suppress("ConstantConditionIf") 31 | class MainActivity : AppCompatActivity() { 32 | 33 | companion object { 34 | //this flag is used to hide UI elements 35 | //for screen recording purposes 36 | const val showcaseMode = false 37 | 38 | //this flag toggles the views' height 39 | //between match width and match parent 40 | var squareItems = true 41 | } 42 | 43 | private val data by lazy { Data(isConnected()) } 44 | 45 | private val stackLayoutManager by lazy { 46 | StackLayoutManager( 47 | maxViews = 8, 48 | horizontalLayout = false, 49 | scrollMultiplier = 1.5f 50 | ) 51 | } 52 | private val recyclerView by lazy { 53 | findViewById(R.id.recyclerView) 54 | } 55 | private val snapHelper = object : RecyclerView.OnScrollListener() { 56 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { 57 | super.onScrollStateChanged(recyclerView, newState) 58 | if (newState != SCROLL_STATE_DRAGGING) { 59 | stackLayoutManager.snap() 60 | } 61 | } 62 | } 63 | private var attachedSnapHelper = false 64 | private val glide by lazy { 65 | Glide.with(this) 66 | } 67 | private var currentTransformer = 0 68 | 69 | @SuppressLint("MissingSuperCall") 70 | override fun onCreate(savedInstanceState: Bundle?) { 71 | super.onCreate(savedInstanceState) 72 | setContentView(R.layout.activity_main) 73 | 74 | recyclerView.apply { 75 | hasFixedSize() 76 | layoutManager = stackLayoutManager 77 | adapter = CardsAdapter() 78 | postDelayed({ stackLayoutManager.peek() }, 1000) 79 | } 80 | setUpDemo() 81 | } 82 | 83 | private fun hideUiElementsIfShowcase() { 84 | if (!showcaseMode) return 85 | val views = arrayOf( 86 | R.id.background, 87 | R.id.landscape, 88 | R.id.attention, 89 | R.id.snap, 90 | R.id.interpolator, 91 | R.id.transformer, 92 | R.id.guideline, 93 | R.id.size_fullscreen, 94 | R.id.size_square 95 | ) 96 | for (id in views) { 97 | findViewById(id).alpha = 98 | if (showcaseMode) 0f else 1f 99 | } 100 | 101 | findViewById(R.id.recyclerView).layoutParams.height = -1 102 | findViewById(R.id.container).setBackgroundColor(Color.WHITE) 103 | 104 | window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE 105 | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 106 | or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 107 | or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 108 | or View.SYSTEM_UI_FLAG_FULLSCREEN 109 | or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) 110 | } 111 | 112 | private fun changeItemsSize(square: Boolean) { 113 | if (squareItems == square) return 114 | squareItems = square 115 | stackLayoutManager.forceRemeasureInNextLayout() 116 | recyclerView.adapter?.notifyItemRangeChanged( 117 | recyclerView.getChildAdapterPosition( 118 | recyclerView.getChildAt(0) 119 | ), recyclerView.childCount 120 | ) 121 | toast("${if (squareItems) "Square" else "Full"} mode") 122 | } 123 | 124 | private fun setUpDemo() { 125 | hideUiElementsIfShowcase() 126 | 127 | findViewById(R.id.size_square).setOnClickListener { 128 | changeItemsSize(true) 129 | } 130 | 131 | findViewById(R.id.size_fullscreen).setOnClickListener { 132 | changeItemsSize(false) 133 | } 134 | 135 | findViewById(R.id.landscape).setOnClickListener { 136 | resetViews() 137 | stackLayoutManager.horizontalLayout = !stackLayoutManager.horizontalLayout 138 | toast("Showing in ${if (stackLayoutManager.horizontalLayout) "horizontal" else "vertical"} mode") 139 | } 140 | 141 | findViewById(R.id.attention).setOnClickListener { 142 | stackLayoutManager.peek() 143 | } 144 | 145 | findViewById(R.id.snap).setOnClickListener { 146 | if (!attachedSnapHelper) { 147 | recyclerView.addOnScrollListener(snapHelper) 148 | stackLayoutManager.snap() 149 | } else recyclerView.removeOnScrollListener(snapHelper) 150 | attachedSnapHelper = !attachedSnapHelper 151 | toast("Snapping ${if (attachedSnapHelper) "enabled" else "disabled"}") 152 | } 153 | 154 | findViewById(R.id.interpolator).setOnClickListener { 155 | resetViews() 156 | val nextInterpolator = 157 | layoutInterpolators.indexOf(stackLayoutManager.layoutInterpolator) + 1 158 | val interpolatorObj = layoutInterpolators[nextInterpolator % layoutInterpolators.size] 159 | if (interpolatorObj is ReverseStackInterpolator) { 160 | //ReverseStackInterpolator uses its own transformer 161 | //to allow most recent views (higher position) to be stacked underneath 162 | //current view by reversing the translationZ property 163 | stackLayoutManager.viewTransformer = 164 | ReverseStackInterpolator.ReverseStackTransformer::transform 165 | } else if (stackLayoutManager.viewTransformer == ReverseStackInterpolator.ReverseStackTransformer::transform) { 166 | //if ReverseStackInterpolator transformer was previously set, revert to the last used 167 | //transformer 168 | stackLayoutManager.viewTransformer = 169 | transformers[currentTransformer % transformers.size] 170 | resetViews() 171 | } 172 | stackLayoutManager.layoutInterpolator = interpolatorObj 173 | toast("Current interpolator: ${interpolatorObj.javaClass.simpleName}") 174 | } 175 | findViewById(R.id.transformer).setOnClickListener { 176 | if (stackLayoutManager.layoutInterpolator is ReverseStackInterpolator) { 177 | toast("The path currently set does not support transformation. Please choose another path") 178 | return@setOnClickListener 179 | } 180 | resetViews() 181 | currentTransformer++ 182 | stackLayoutManager.viewTransformer = 183 | transformers[currentTransformer % transformers.size] 184 | toast( 185 | "Current transform: ${data.transformersNames[currentTransformer % data.transformersNames.size]}" 186 | ) 187 | } 188 | } 189 | 190 | private fun resetViews() { 191 | recyclerView.stopScroll() 192 | recyclerView.children.forEach { 193 | it.resetTransformations() 194 | } 195 | } 196 | 197 | //manually remove all transformations made to a view 198 | //we need to do this manually since the layout manager 199 | //does not know what transformations were made to a view 200 | private fun View?.resetTransformations() { 201 | this?.apply { 202 | scaleY = 1f 203 | scaleX = 1f 204 | translationX = 0f 205 | translationY = 0f 206 | elevation = 0f 207 | translationZ = 0f 208 | rotationX = 0f 209 | rotationY = 0f 210 | } 211 | } 212 | 213 | private fun toast(text: String) { 214 | Snackbar.make(recyclerView, text, 4000).setAnchorView(R.id.background).show() 215 | } 216 | 217 | private val transformers by lazy { 218 | arrayOf( 219 | //this is the default transformer supplied by StackLayoutManager 220 | StackLayoutManager.ElevationTransformer::transform, 221 | //scale out the view as it exists the screen 222 | ScaleOutOnlyTransformer::transform, 223 | //scale in the view as it enters the screen 224 | ScaleInOnlyTransformer::transform, 225 | //a combination of scale-out and scale-in transformers 226 | ScaleTransformer::transform, 227 | //rotate the view along either of its axes 228 | RotationTransformer::transform 229 | ) 230 | } 231 | 232 | private val layoutInterpolators by lazy { 233 | arrayOf( 234 | LinearInterpolator(), 235 | DenseStackInterpolator(), 236 | ReverseStackInterpolator(), 237 | LogDecelerateInterpolator(60f, 0), 238 | OvershootingInterpolator(), 239 | CappedLinearInterpolator() 240 | ) 241 | } 242 | 243 | inner class CardsAdapter : RecyclerView.Adapter(), View.OnClickListener, 244 | View.OnLongClickListener { 245 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardsViewHolder { 246 | return CardsViewHolder( 247 | LayoutInflater.from(parent.context) 248 | .inflate(R.layout.recycler_view_item, parent, false) 249 | ) 250 | } 251 | 252 | init { 253 | setHasStableIds(true) 254 | } 255 | 256 | override fun onViewRecycled(holder: CardsViewHolder) { 257 | super.onViewRecycled(holder) 258 | glide.clear(holder.thumbImageView) 259 | } 260 | 261 | override fun getItemId(position: Int): Long { 262 | //this is fine for demo purposes but 263 | //more advanced identification is usually needed 264 | //for complex data 265 | return position.toLong() 266 | } 267 | 268 | override fun getItemCount(): Int { 269 | return 101 270 | } 271 | 272 | override fun onBindViewHolder(holder: CardsViewHolder, position: Int) { 273 | holder.itemView.resetTransformations() 274 | holder.card.setOnClickListener(this) 275 | holder.card.setOnLongClickListener(this) 276 | holder.positionTextView.text = position.toString() 277 | glide 278 | .load(data.images[position % data.images.size]) 279 | .format(DecodeFormat.PREFER_ARGB_8888) 280 | .centerCrop() 281 | .thumbnail(0.3f) 282 | .into(holder.thumbImageView) 283 | } 284 | 285 | override fun onClick(v: View?) { 286 | v ?: return 287 | recyclerView?.also { recyclerView -> 288 | val position = recyclerView.getChildViewHolder(v)?.adapterPosition ?: -1 289 | if (position >= 0) { 290 | (recyclerView.layoutManager as? StackLayoutManager)?.scrollToPosition(position) 291 | } 292 | } 293 | } 294 | 295 | override fun onLongClick(v: View?): Boolean { 296 | v ?: return false 297 | recyclerView?.also { recyclerView -> 298 | val position = recyclerView.getChildViewHolder(v)?.adapterPosition ?: -1 299 | if (position >= 0) { 300 | (recyclerView.layoutManager as? StackLayoutManager)?.scrollToPosition( 301 | position + data.images.size.toFloat(), 302 | animated = true, 303 | duration = data.images.size * 250L 304 | ) 305 | } 306 | } 307 | return true 308 | } 309 | 310 | } 311 | 312 | class CardsViewHolder(view: View) : RecyclerView.ViewHolder(view) { 313 | val card = view as Card 314 | val thumbImageView = view.findViewById(R.id.thumb) as AppCompatImageView 315 | val positionTextView = (view.findViewById(R.id.position) as TextView).also { 316 | it.alpha = if (showcaseMode) 0f else 1f 317 | } 318 | 319 | } 320 | 321 | //simple internet check 322 | private fun isConnected(): Boolean { 323 | val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 324 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 325 | return cm.activeNetwork != null 326 | } 327 | val nInfo = cm.activeNetworkInfo 328 | return nInfo != null && nInfo.isAvailable && nInfo.isConnected 329 | } 330 | 331 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/card_bg.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/header_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrows.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 20 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_aspect_ratio_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_crop_square_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bullseye_gradient.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_confetti_doodles.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cornered_stairs.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_curve_www_wishforge_games.xml: -------------------------------------------------------------------------------- 1 | 7 | 14 | 21 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_eye.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_flat_mountains.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_geometric_intersection.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 61 | 65 | 69 | 73 | 77 | 81 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 113 | 117 | 121 | 125 | 126 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_hollowed_boxes.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_magnet.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_rainbow_vortex.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 15 | 21 | 27 | 33 | 39 | 45 | 51 | 57 | 63 | 69 | 75 | 81 | 87 | 93 | 99 | 105 | 111 | 117 | 118 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_repeating_chevrons.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_scale_royyan_wijaya.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/font/poppins_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/share_tech_mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/font/share_tech_mono.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 20 | 21 | 27 | 28 | 37 | 38 | 57 | 58 | 59 | 77 | 78 | 95 | 96 | 113 | 114 | 115 | 133 | 134 | 135 | 148 | 149 | 150 | 163 | 164 | -------------------------------------------------------------------------------- /app/src/main/res/layout/recycler_view_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #121212 4 | #333 5 | #fff 6 | #fff 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | StackLayoutManager 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = '1.5.21' 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath "com.android.tools.build:gradle:4.1.3" 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | task clean(type: Delete) { 25 | delete rootProject.buildDir 26 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':stacklayoutmanager' 2 | include ':app' 3 | rootProject.name = "StackLayoutManager" 4 | include ':stacklayoutmanager-extras' 5 | -------------------------------------------------------------------------------- /stacklayoutmanager-extras/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /stacklayoutmanager-extras/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 30 8 | 9 | defaultConfig { 10 | minSdk 16 11 | targetSdk 30 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | dependencies { 35 | compileOnly project(path: ':stacklayoutmanager') 36 | implementation 'androidx.core:core-ktx:1.6.0' 37 | implementation 'androidx.appcompat:appcompat:1.3.1' 38 | implementation 'com.google.android.material:material:1.4.0' 39 | testImplementation 'junit:junit:4.+' 40 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 41 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 42 | } -------------------------------------------------------------------------------- /stacklayoutmanager-extras/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/stacklayoutmanager-extras/consumer-rules.pro -------------------------------------------------------------------------------- /stacklayoutmanager-extras/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/layoutinterpolators/CappedLinearInterpolator.kt: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.layoutinterpolators 2 | 3 | import android.view.animation.LinearInterpolator 4 | 5 | /** 6 | * Just like with OvershootingInterpolator, this interpolator doesn't allow 7 | * the view to go offscreen once interpolation has reached the end (aka clamp value) 8 | * 9 | * Preview: https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/capped_linear_interpolator.webp 10 | */ 11 | class CappedLinearInterpolator : LinearInterpolator() { 12 | override fun getInterpolation(t: Float): Float { 13 | return super.getInterpolation(kotlin.math.min(1f, t)) 14 | } 15 | } -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/layoutinterpolators/DenseStackInterpolator.kt: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.layoutinterpolators 2 | 3 | import android.graphics.Path 4 | 5 | /** 6 | * This interpolator recreates the original behaviour that was intended by Google in their TaskStackLayout 7 | * @param stackedViews: the number of views shown at once. 8 | * 3f means we want to have three views shown inside the recyclerview 9 | * Preview: https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/dense_interpolator.webp 10 | */ 11 | class DenseStackInterpolator(private val stackedViews: Float = 3f) : 12 | FreePathInterpolator(layoutPath) { 13 | override fun getInterpolation(t: Float): Float { 14 | return super.getInterpolation(t / stackedViews) 15 | } 16 | 17 | companion object { 18 | private val layoutPath: Path by lazy { 19 | Path().apply { 20 | /* 21 | simple interpretation of this is: 22 | when x equals to [value], y should be [value] 23 | where y=1 --> view is at its final position 24 | y = 0 --> offscreen, with 0 displacement 25 | y = -0.5 --> offscreen, with displacement equals to (1f-(-0.5f)) of the view length 26 | which means 1.5f * view width or height depending on the current mode 27 | similarly, y = -1.5 --> the view is located offscreen, with displacement 28 | that is equal to 2.5x its length 29 | */ 30 | moveTo(0f, -0.5f) 31 | quadTo(0.1f, 0.5f, 0.2f, 0.95f) 32 | quadTo(0.2f, 1f, 1f, 1.03f) 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/layoutinterpolators/FreePathInterpolator.java: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.layoutinterpolators; 2 | 3 | 4 | import android.graphics.Path; 5 | import android.graphics.PathMeasure; 6 | import android.os.Build; 7 | 8 | import androidx.annotation.FloatRange; 9 | import androidx.annotation.NonNull; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * This class allows the path to end at points farther than 1 16 | * while the stock PathInterpolator doesn't. Therefore, you can 17 | * create more sophisticated layout interpolators using this class 18 | * Originally developed for SystemUI stack view 19 | *

20 | * Since path approximate is only public on Oreo+, we use a backport 21 | * courtesy of alexjlockwood 22 | */ 23 | public class FreePathInterpolator implements android.view.animation.Interpolator { 24 | // This governs how accurate the approximation of the Path is. 25 | private static final float PRECISION = 0.002f; 26 | private float[] mX; 27 | private float[] mY; 28 | private float mArcLength; 29 | /** 30 | * Create an interpolator for an arbitrary Path. 31 | * 32 | * @param path The Path to use to make the line representing the interpolator. 33 | */ 34 | public FreePathInterpolator(Path path) { 35 | initPath(path); 36 | } 37 | 38 | private float[] approximate(Path p, float PRECISION) { 39 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 40 | return p.approximate(PRECISION); 41 | } else 42 | return PathCompat.approximate(p, PRECISION); 43 | } 44 | 45 | private void initPath(Path path) { 46 | float[] pointComponents = approximate(path, PRECISION); 47 | 48 | int numPoints = pointComponents.length / 3; 49 | 50 | mX = new float[numPoints]; 51 | mY = new float[numPoints]; 52 | mArcLength = 0; 53 | float prevX = 0; 54 | float prevY = 0; 55 | float prevFraction = 0; 56 | int componentIndex = 0; 57 | for (int i = 0; i < numPoints; i++) { 58 | float fraction = pointComponents[componentIndex++]; 59 | float x = pointComponents[componentIndex++]; 60 | float y = pointComponents[componentIndex++]; 61 | if (fraction == prevFraction && x != prevX) { 62 | throw new IllegalArgumentException( 63 | "The Path cannot have discontinuity in the X axis."); 64 | } 65 | if (x < prevX) { 66 | throw new IllegalArgumentException("The Path cannot loop back on itself."); 67 | } 68 | mX[i] = x; 69 | mY[i] = y; 70 | mArcLength += Math.hypot(x - prevX, y - prevY); 71 | prevX = x; 72 | prevY = y; 73 | prevFraction = fraction; 74 | } 75 | } 76 | 77 | /** 78 | * Using the line in the Path in this interpolator that can be described as 79 | * y = f(x), finds the y coordinate of the line given t 80 | * as the x coordinate. 81 | * 82 | * @param t Treated as the x coordinate along the line. 83 | * @return The y coordinate of the Path along the line where x = t. 84 | */ 85 | @Override 86 | public float getInterpolation(float t) { 87 | int startIndex = 0; 88 | int endIndex = mX.length - 1; 89 | 90 | // Return early if out of bounds 91 | if (t <= 0) { 92 | return mY[startIndex]; 93 | } else if (t >= 1) { 94 | return mY[endIndex]; 95 | } 96 | 97 | // Do a binary search for the correct x to interpolate between. 98 | while (endIndex - startIndex > 1) { 99 | int midIndex = (startIndex + endIndex) / 2; 100 | if (t < mX[midIndex]) { 101 | endIndex = midIndex; 102 | } else { 103 | startIndex = midIndex; 104 | } 105 | } 106 | 107 | float xRange = mX[endIndex] - mX[startIndex]; 108 | if (xRange == 0) { 109 | return mY[startIndex]; 110 | } 111 | 112 | float tInRange = t - mX[startIndex]; 113 | float fraction = tInRange / xRange; 114 | 115 | float startY = mY[startIndex]; 116 | float endY = mY[endIndex]; 117 | return startY + (fraction * (endY - startY)); 118 | } 119 | 120 | /** 121 | * Finds the x that provides the given y = f(x). 122 | * 123 | * @param y a value from (0,1) that is in this path. 124 | */ 125 | public float getX(float y) { 126 | int startIndex = 0; 127 | int endIndex = mY.length - 1; 128 | 129 | // Return early if out of bounds 130 | if (y <= 0) { 131 | return mX[endIndex]; 132 | } else if (y >= 1) { 133 | return mX[startIndex]; 134 | } 135 | 136 | // Do a binary search for index that bounds the y 137 | while (endIndex - startIndex > 1) { 138 | int midIndex = (startIndex + endIndex) / 2; 139 | if (y < mY[midIndex]) { 140 | startIndex = midIndex; 141 | } else { 142 | endIndex = midIndex; 143 | } 144 | } 145 | 146 | float yRange = mY[endIndex] - mY[startIndex]; 147 | if (yRange == 0) { 148 | return mX[startIndex]; 149 | } 150 | 151 | float tInRange = y - mY[startIndex]; 152 | float fraction = tInRange / yRange; 153 | 154 | float startX = mX[startIndex]; 155 | float endX = mX[endIndex]; 156 | return startX + (fraction * (endX - startX)); 157 | } 158 | 159 | /** 160 | * Returns the arclength of the path we are interpolating. 161 | */ 162 | public float getArcLength() { 163 | return mArcLength; 164 | } 165 | 166 | //https://gist.github.com/alexjlockwood/7d3685fe9ce7dcfde33112c4e6c5ce4f 167 | private static final class PathCompat { 168 | private static final int MAX_NUM_POINTS = 100; 169 | private static final int FRACTION_OFFSET = 0; 170 | private static final int X_OFFSET = 1; 171 | private static final int Y_OFFSET = 2; 172 | private static final int NUM_COMPONENTS = 3; 173 | 174 | private PathCompat() { 175 | } 176 | 177 | /** 178 | * Approximate the Path with a series of line segments. 179 | * This returns float[] with the array containing point components. 180 | * There are three components for each point, in order: 181 | *

    182 | *
  • Fraction along the length of the path that the point resides
  • 183 | *
  • The x coordinate of the point
  • 184 | *
  • The y coordinate of the point
  • 185 | *
186 | *

Two points may share the same fraction along its length when there is 187 | * a move action within the Path.

188 | * 189 | * @param acceptableError The acceptable error for a line on the 190 | * Path. Typically this would be 0.5 so that 191 | * the error is less than half a pixel. 192 | * @return An array of components for points approximating the Path. 193 | */ 194 | @NonNull 195 | public static float[] approximate(@NonNull Path path, @FloatRange(from = 0f) float acceptableError) { 196 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 197 | return path.approximate(acceptableError); 198 | } 199 | if (acceptableError < 0) { 200 | throw new IllegalArgumentException("acceptableError must be greater than or equal to 0"); 201 | } 202 | // Measure the total length the whole pathData. 203 | final PathMeasure measureForTotalLength = new PathMeasure(path, false); 204 | float totalLength = 0; 205 | // The sum of the previous contour plus the current one. Using the sum here 206 | // because we want to directly subtract from it later. 207 | final List summedContourLengths = new ArrayList<>(); 208 | summedContourLengths.add(0f); 209 | do { 210 | final float pathLength = measureForTotalLength.getLength(); 211 | totalLength += pathLength; 212 | summedContourLengths.add(totalLength); 213 | } while (measureForTotalLength.nextContour()); 214 | 215 | // Now determine how many sample points we need, and the step for next sample. 216 | final PathMeasure pathMeasure = new PathMeasure(path, false); 217 | 218 | final int numPoints = Math.min(MAX_NUM_POINTS, (int) (totalLength / acceptableError) + 1); 219 | 220 | final float[] coords = new float[NUM_COMPONENTS * numPoints]; 221 | final float[] position = new float[2]; 222 | 223 | int contourIndex = 0; 224 | final float step = totalLength / (numPoints - 1); 225 | float cumulativeDistance = 0; 226 | 227 | // For each sample point, determine whether we need to move on to next contour. 228 | // After we find the right contour, then sample it using the current distance value minus 229 | // the previously sampled contours' total length. 230 | for (int i = 0; i < numPoints; i++) { 231 | // The cumulative distance traveled minus the total length of the previous contours 232 | // (not including the current contour). 233 | final float contourDistance = cumulativeDistance - summedContourLengths.get(contourIndex); 234 | pathMeasure.getPosTan(contourDistance, position, null); 235 | 236 | coords[i * NUM_COMPONENTS + FRACTION_OFFSET] = cumulativeDistance / totalLength; 237 | coords[i * NUM_COMPONENTS + X_OFFSET] = position[0]; 238 | coords[i * NUM_COMPONENTS + Y_OFFSET] = position[1]; 239 | 240 | cumulativeDistance = Math.min(cumulativeDistance + step, totalLength); 241 | 242 | // Using a while statement is necessary in the rare case where step is greater than 243 | // the length a path contour. 244 | while (summedContourLengths.get(contourIndex + 1) < cumulativeDistance) { 245 | contourIndex++; 246 | pathMeasure.nextContour(); 247 | } 248 | } 249 | 250 | coords[(numPoints - 1) * NUM_COMPONENTS + FRACTION_OFFSET] = 1f; 251 | return coords; 252 | } 253 | } 254 | } -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/layoutinterpolators/LogDecelerateInterpolator.java: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.layoutinterpolators; 2 | 3 | import android.animation.TimeInterpolator; 4 | 5 | /** 6 | * Standard ease out interpolator from Launcher3 package 7 | *

8 | * Preview: https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/log_decelerate_60_interpolator.webp 9 | */ 10 | public class LogDecelerateInterpolator implements TimeInterpolator { 11 | private final float mBase; 12 | private final float mDrift; 13 | private final float mTimeScale; 14 | private final float mOutputScale; 15 | 16 | public LogDecelerateInterpolator(float base, int drift) { 17 | mBase = base; 18 | mDrift = drift; 19 | mTimeScale = 1f; 20 | mOutputScale = 1f / computeLog(1f); 21 | } 22 | 23 | private float computeLog(float t) { 24 | return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t); 25 | } 26 | 27 | @Override 28 | public float getInterpolation(float t) { 29 | return computeLog(t) * mOutputScale; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/layoutinterpolators/OvershootingInterpolator.kt: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.layoutinterpolators 2 | 3 | import android.graphics.Path 4 | 5 | /** 6 | * Similar to OvershootInterpolator but this implementation 7 | * makes sure that the view stays inside the screen when 8 | * the interpolation reaches 1 9 | * 10 | * Preview: https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/overshooting_interpolator.webp 11 | */ 12 | class OvershootingInterpolator : FreePathInterpolator(layoutPath) { 13 | companion object { 14 | private val layoutPath: Path = Path().apply { 15 | //y is -1 to make sure that that the view doesn't remain 16 | // static at the bottom of the screen when vertical mode is activated 17 | // it basically means the view is displaced 1x its length offscreen 18 | moveTo(0f, -1f) 19 | //y = 1.05f means the overshoot should be 0.05f of the view's length 20 | quadTo(0.4f, 0.5f, 0.7f, 1.05f) 21 | lineTo(1f, 1f) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/layoutinterpolators/ReverseStackInterpolator.kt: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.layoutinterpolators 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Path 5 | import android.os.Build 6 | import android.util.TypedValue 7 | import android.view.View 8 | import android.view.animation.LinearInterpolator 9 | import library.StackLayoutManager 10 | import kotlin.math.max 11 | 12 | /** 13 | * This interpolator draws views in reverse 14 | * 15 | * Preview: https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/reverse_interpolator.webp 16 | */ 17 | 18 | class ReverseStackInterpolator : LinearInterpolator() { 19 | private val path = FreePathInterpolator(Path().apply { 20 | moveTo(0f, 0.9f) 21 | lineTo(1f, 0.95f) 22 | lineTo(2f, 3f) 23 | }) 24 | 25 | object ReverseStackTransformer { 26 | private val maxTranslationZ by lazy { 27 | TypedValue.applyDimension( 28 | TypedValue.COMPLEX_UNIT_DIP, 29 | 20f, 30 | Resources.getSystem().displayMetrics 31 | ) 32 | } 33 | 34 | fun transform(x: Float, v: View, stackLayoutManager: StackLayoutManager) { 35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 36 | v.elevation = maxTranslationZ 37 | v.translationZ = (1f - x) * 2f 38 | } 39 | } 40 | } 41 | 42 | override fun getInterpolation(input: Float): Float { 43 | return max((0.95f + input * 0.05f).coerceAtLeast(0.95f), input) 44 | } 45 | } -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/transformers/RotationTransformer.kt: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.transformers 2 | 3 | import android.graphics.Path 4 | import android.os.Build 5 | import android.view.View 6 | import library.StackLayoutManager 7 | import library.stacklayoutmanager.extras.layoutinterpolators.FreePathInterpolator 8 | 9 | /** 10 | * This transformer gives the same effect as the AOSP gallery home screen widget 11 | * 12 | * Preview: https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/3d_transform.webp 13 | */ 14 | object RotationTransformer { 15 | private val scalePath = 16 | FreePathInterpolator( 17 | Path().apply { 18 | moveTo(0f, 0.4f) 19 | lineTo(0.6f, 1f) 20 | lineTo(1f, 1f) 21 | }) 22 | 23 | private val rotationPath = 24 | FreePathInterpolator( 25 | Path().apply { 26 | moveTo(0f, 0f) 27 | //rotation is stopped past 60% 28 | lineTo(0.6f, 1f) 29 | lineTo(1f, 1f) 30 | }) 31 | 32 | fun transform(x: Float, v: View, stackLayoutManager: StackLayoutManager) { 33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 34 | v.translationZ = 0f 35 | } 36 | val scale = scalePath.getInterpolation(1f - x) 37 | val rotation = 1f - rotationPath.getInterpolation(1f - x) 38 | v.pivotY = v.height.toFloat() / 2f 39 | v.pivotX = v.width.toFloat() / 2f 40 | //using rotation to reduce scale 41 | //ensures that all of the view remains 42 | //inside the screen while transforming 43 | v.scaleX = scale * (1f - rotation) 44 | v.scaleY = scale * (1f - rotation) 45 | if (stackLayoutManager.horizontalLayout) 46 | v.rotationY = rotation * 90f 47 | else v.rotationX = -rotation * 90f 48 | } 49 | } -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/transformers/ScaleInOnlyTransformer.kt: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.transformers 2 | 3 | import android.graphics.Path 4 | import android.view.View 5 | import library.StackLayoutManager 6 | import library.stacklayoutmanager.extras.layoutinterpolators.FreePathInterpolator 7 | 8 | /** 9 | * This transformer scales the view only during entry 10 | * 11 | * Preview: https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/scale_in_transform.webp 12 | */ 13 | object ScaleInOnlyTransformer { 14 | private val scalePath = 15 | FreePathInterpolator( 16 | Path().apply { 17 | //0.7f is the minimum scale 18 | moveTo(0f, 0.7f) 19 | lineTo(1f, 1f) 20 | }) 21 | 22 | fun transform(x: Float, v: View, stackLayoutManager: StackLayoutManager) { 23 | StackLayoutManager.ElevationTransformer.transform(x, v, stackLayoutManager) 24 | val scale = if (x <= 0f) 1f else scalePath.getInterpolation(1f - kotlin.math.abs(x)) 25 | v.scaleX = scale 26 | v.scaleY = scale 27 | } 28 | } -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/transformers/ScaleOutOnlyTransformer.kt: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.transformers 2 | 3 | import android.graphics.Path 4 | import android.view.View 5 | import library.StackLayoutManager 6 | import library.stacklayoutmanager.extras.layoutinterpolators.FreePathInterpolator 7 | 8 | /** 9 | * This transformer gives the same effect as the Shazam android app (an old version, global chart cards) 10 | * 11 | * Preview: https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/scale_out_transform.webp 12 | */ 13 | object ScaleOutOnlyTransformer { 14 | private val scalePath = 15 | FreePathInterpolator( 16 | Path().apply { 17 | //0.7f is the minimum scale 18 | moveTo(0f, 0.7f) 19 | lineTo(1f, 1f) 20 | }) 21 | 22 | fun transform(x: Float, v: View, stackLayoutManager: StackLayoutManager) { 23 | StackLayoutManager.ElevationTransformer.transform(x, v, stackLayoutManager) 24 | val scale = if (x > 0f) 1f else scalePath.getInterpolation(1f - kotlin.math.abs(x)) 25 | v.scaleX = scale 26 | v.scaleY = scale 27 | } 28 | } -------------------------------------------------------------------------------- /stacklayoutmanager-extras/src/main/java/library/stacklayoutmanager/extras/transformers/ScaleTransformer.kt: -------------------------------------------------------------------------------- 1 | package library.stacklayoutmanager.extras.transformers 2 | 3 | import android.graphics.Path 4 | import android.view.View 5 | import library.StackLayoutManager 6 | import library.stacklayoutmanager.extras.layoutinterpolators.FreePathInterpolator 7 | 8 | /** 9 | * This transformer scales the view during entry and exit 10 | * 11 | * Preview: https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/master/Showcase/gifs/scale_in_out.webp 12 | */ 13 | object ScaleTransformer { 14 | private val scalePath = 15 | FreePathInterpolator( 16 | Path().apply { 17 | //0.7f is the minimum scale 18 | moveTo(0f, 0.7f) 19 | lineTo(1f, 1f) 20 | }) 21 | 22 | fun transform(x: Float, v: View, stackLayoutManager: StackLayoutManager) { 23 | StackLayoutManager.ElevationTransformer.transform(x, v, stackLayoutManager) 24 | val scale = if (x == 0f) 1f else scalePath.getInterpolation(1f - kotlin.math.abs(x)) 25 | v.scaleX = scale 26 | v.scaleY = scale 27 | } 28 | } -------------------------------------------------------------------------------- /stacklayoutmanager/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /stacklayoutmanager/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | } 4 | apply plugin: 'kotlin-android' 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | minSdkVersion 16 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_7 26 | targetCompatibility JavaVersion.VERSION_1_7 27 | } 28 | 29 | } 30 | 31 | dependencies { 32 | compileOnly 'androidx.appcompat:appcompat:1.3.1' 33 | compileOnly "androidx.recyclerview:recyclerview:1.2.1" 34 | compileOnly "androidx.core:core-ktx:1.6.0" 35 | compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 36 | 37 | } 38 | repositories { 39 | mavenCentral() 40 | } -------------------------------------------------------------------------------- /stacklayoutmanager/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkionAvey/StackLayoutManager/983fb34c0459633723c230b88221f60730cf60a9/stacklayoutmanager/consumer-rules.pro -------------------------------------------------------------------------------- /stacklayoutmanager/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /stacklayoutmanager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /stacklayoutmanager/src/main/java/library/StackLayoutManager.kt: -------------------------------------------------------------------------------- 1 | package library 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.animation.TimeInterpolator 6 | import android.animation.ValueAnimator 7 | import android.content.Context 8 | import android.content.res.Resources 9 | import android.graphics.Rect 10 | import android.os.Build 11 | import android.util.AttributeSet 12 | import android.util.TypedValue 13 | import android.view.View 14 | import android.view.View.MeasureSpec 15 | import android.view.ViewGroup 16 | import android.view.animation.LinearInterpolator 17 | import androidx.core.view.ViewCompat 18 | import androidx.recyclerview.widget.RecyclerView 19 | import androidx.recyclerview.widget.RecyclerView.Recycler 20 | import kotlin.math.max 21 | import kotlin.math.min 22 | import kotlin.math.pow 23 | import kotlin.math.roundToInt 24 | 25 | /** 26 | * A lightweight and highly-customizable, interpolator-based layout manager for RecyclerView 27 | * visit https://github.com/DarkionAvey/StackLayoutManager for more info 28 | */ 29 | class StackLayoutManager( 30 | //toggle between horizontal and vertical modes 31 | horizontalLayout: Boolean = true, 32 | //should first item be centered? 33 | val centerFirstItem: Boolean = true, 34 | //how many items should be scrolled in one edge-to-edge swipe 35 | val scrollMultiplier: Float = 1.2f, 36 | // the max number of children the recyclerview should have 37 | val maxViews: Int = 6, 38 | //interpolator for laying out views 39 | layoutInterpolator: TimeInterpolator = Internal.LAYOUT_INTERPOLATOR, 40 | //supply a higher-order function to receive ViewTransformation object 41 | //for custom view transformations. 42 | //x represents the raw x value that needs to be interpolated into y 43 | viewTransformer: ((x: Float, view: View, stackLayoutManager: StackLayoutManager) -> Unit?)? = ElevationTransformer::transform 44 | 45 | ) : RecyclerView.LayoutManager() { 46 | 47 | var viewTransformer: ((x: Float, view: View, stackLayoutManager: StackLayoutManager) -> Unit?)? = 48 | viewTransformer 49 | set(value) { 50 | //due to the dynamic way view transformation works 51 | //the caller is responsible for resetting the views 52 | //to their original, pre-transformation state 53 | requestSimpleAnimationsInNextLayout() 54 | field = value 55 | requestLayout() 56 | } 57 | var layoutInterpolator = layoutInterpolator 58 | set(value) { 59 | field = value 60 | requestSimpleAnimationsInNextLayout() 61 | requestLayout() 62 | } 63 | private val stopScrollingRunnable by lazy { Runnable { stopScrolling() } } 64 | private var displayRect = Rect() 65 | private val viewRect = Rect() 66 | private val marginsRect = Rect() 67 | private val tmpRect = Rect() 68 | private var scrollAnimator: ValueAnimator? = null 69 | private val stackAlgorithm: LayoutAlgorithm = LayoutAlgorithm() 70 | private val scroller: StackScroller = StackScroller() 71 | private var decoratedChildHeight = -1 72 | private val firstItemPosition: Int 73 | get() = max( 74 | 0, 75 | min(itemCount - maxViews, currentItem - maxViews / 2) 76 | ) 77 | private val lastVisibleItemPosition: Int 78 | get() = if (firstItemPosition == 0) min( 79 | maxViews, 80 | itemCount 81 | ) else min(itemCount, currentItem + maxViews / 2) 82 | 83 | var horizontalLayout: Boolean = horizontalLayout 84 | set(value) { 85 | if (field == value) return 86 | field = value 87 | requestSimpleAnimationsInNextLayout() 88 | displayRect.setEmpty() 89 | requestLayout() 90 | } 91 | 92 | //return how far has the recyclerview been scrolled relative to its length 93 | val relativeScroll: Float 94 | get() = scroller.stackScroll 95 | 96 | //return how far has the recyclerview been scrolled in pixels 97 | val absoluteScroll: Float 98 | get() = stackAlgorithm.getYForDeltaP(scroller.stackScroll).toFloat() 99 | 100 | //return currently focused item 101 | val currentItem: Int 102 | get() = min( 103 | max( 104 | 0, 105 | kotlin.math.floor( 106 | scroller.stackScroll.toDouble() 107 | ).toInt() 108 | ), itemCount 109 | ) 110 | 111 | override fun isSmoothScrolling(): Boolean { 112 | return super.isSmoothScrolling() || scrollAnimator != null && scrollAnimator!!.isRunning 113 | } 114 | 115 | override fun scrollVerticallyBy( 116 | dy: Int, 117 | recycler: Recycler, 118 | state: RecyclerView.State 119 | ): Int { 120 | return scroll(dy.toFloat(), recycler, state) 121 | } 122 | 123 | override fun scrollHorizontallyBy( 124 | dx: Int, 125 | recycler: Recycler, 126 | state: RecyclerView.State 127 | ): Int { 128 | return scroll(dx.toFloat(), recycler, state) 129 | } 130 | 131 | override fun canScrollVertically(): Boolean { 132 | return itemCount != 0 && !horizontalLayout 133 | } 134 | 135 | override fun canScrollHorizontally(): Boolean { 136 | return itemCount != 0 && horizontalLayout 137 | } 138 | 139 | private fun scroll( 140 | thisMuch: Float, 141 | recycler: Recycler, 142 | state: RecyclerView.State 143 | ): Int { 144 | var delta = thisMuch 145 | stopScrolling() 146 | 147 | if (childCount == 0) { 148 | return 0 149 | } 150 | val deltaP = stackAlgorithm.getDeltaPForY(delta) 151 | var scrollCurrent = scroller.stackScroll + deltaP 152 | if (scrollCurrent < 0) { 153 | scrollCurrent = 0f 154 | delta = 0f 155 | } else if (scrollCurrent >= itemCount - 1) { 156 | scrollCurrent = itemCount - 1.toFloat() 157 | delta = 0f 158 | } 159 | scroller.stackScroll = scrollCurrent 160 | onLayoutChildren(recycler, state) 161 | return delta.toInt() 162 | } 163 | 164 | override fun scrollToPosition(position: Int) { 165 | scrollToPosition(position.toFloat(), true, null) 166 | } 167 | 168 | fun scrollToPosition( 169 | position: Float, 170 | animated: Boolean = true, 171 | duration: Long? = null, 172 | endRunnable: Runnable? = null 173 | ) { 174 | animateScrollToItem(position, endRunnable) 175 | .also { 176 | if (animated.not()) 177 | it.duration = 0 178 | else if (duration != null) 179 | it.duration = duration 180 | } 181 | .start() 182 | } 183 | 184 | private fun animateScrollToItem( 185 | toItem: Float, 186 | postRunnable: Runnable? 187 | ): ValueAnimator { 188 | stopScrolling() 189 | return ValueAnimator.ofFloat(scroller.stackScroll, toItem).apply { 190 | addUpdateListener { animation -> 191 | scroller.stackScroll = animation.animatedValue as Float 192 | requestLayout() 193 | } 194 | if (postRunnable != null) 195 | addListener(object : 196 | AnimatorListenerAdapter() { 197 | override fun onAnimationEnd(animation: Animator) { 198 | super.onAnimationEnd(animation) 199 | postRunnable.run() 200 | } 201 | }) 202 | interpolator = Internal.SCROLL_INTERPOLATOR 203 | duration = (150 * kotlin.math.abs(toItem - scroller.stackScroll)).roundToInt() 204 | .toLong() 205 | }.also { animator -> 206 | scrollAnimator = animator 207 | } 208 | } 209 | 210 | override fun onMeasure( 211 | recycler: Recycler, 212 | state: RecyclerView.State, 213 | widthMeasureSpec: Int, 214 | heightMeasureSpec: Int 215 | ) { 216 | super.onMeasure(recycler, state, widthMeasureSpec, heightMeasureSpec) 217 | if (state.itemCount <= 0) return 218 | setMeasuredDimension( 219 | MeasureSpec.getSize(widthMeasureSpec), 220 | MeasureSpec.getSize(heightMeasureSpec) 221 | ) 222 | } 223 | 224 | override fun onLayoutChildren( 225 | recycler: Recycler, 226 | state: RecyclerView.State 227 | ) { 228 | if (state.itemCount <= 0) { 229 | removeAndRecycleAllViews(recycler) 230 | return 231 | } 232 | 233 | if (updateDisplayRect()) { 234 | val v = recycler.getViewForPosition(0) 235 | addDisappearingView(v) 236 | measureChildWithMargins(v, 0, 0) 237 | val childWidth = v.measuredWidth 238 | decoratedChildHeight = v.measuredHeight 239 | val left: Int 240 | val top: Int 241 | val right: Int 242 | val bottom: Int 243 | val additionalPadding = 0 244 | left = 245 | additionalPadding + paddingStart + kotlin.math.abs(displayRect.width() - childWidth) / 2 246 | top = additionalPadding + paddingTop + (displayRect.height() - decoratedChildHeight) / 2 247 | right = childWidth - paddingEnd - additionalPadding 248 | bottom = decoratedChildHeight - additionalPadding 249 | viewRect.setEmpty() 250 | viewRect[left, top, left + right] = top + bottom 251 | if (v.right == 0) { 252 | v.left = 0 253 | v.right = childWidth 254 | } 255 | if (v.bottom == 0) { 256 | v.top = 0 257 | v.bottom = decoratedChildHeight 258 | } 259 | getDecoratedBoundsWithMargins(v, marginsRect) 260 | removeAndRecycleView(v, recycler) 261 | } 262 | stackAlgorithm.update(lastVisibleItemPosition) 263 | bindVisibleViews(recycler, state) 264 | } 265 | 266 | override fun onAdapterChanged( 267 | oldAdapter: RecyclerView.Adapter<*>?, 268 | newAdapter: RecyclerView.Adapter<*>? 269 | ) { 270 | super.onAdapterChanged(oldAdapter, newAdapter) 271 | removeAllViews() 272 | } 273 | 274 | fun forceRemeasureInNextLayout() { 275 | displayRect.setEmpty() 276 | viewRect.setEmpty() 277 | } 278 | 279 | private fun updateDisplayRect(): Boolean { 280 | if (displayRect.width() == 0 || 281 | displayRect.height() == 0 || 282 | displayRect.width() != width - paddingEnd || 283 | displayRect.height() != height - paddingBottom 284 | ) { 285 | displayRect.left = paddingStart 286 | displayRect.top = paddingTop 287 | displayRect.right = width - paddingEnd 288 | displayRect.bottom = height - paddingBottom 289 | return true 290 | } 291 | return false 292 | } 293 | 294 | private fun bindVisibleViews(recycler: Recycler, state: RecyclerView.State) { 295 | val currentLowerLimit = firstItemPosition 296 | val currentUpperLimit = lastVisibleItemPosition 297 | if (scroller.isScrollOutOfBounds) { 298 | scroller.boundScroll() 299 | } 300 | for (i in childCount - 1 downTo 0) { 301 | val tv = getChildAt(i) ?: continue 302 | detachAndScrapView(tv, recycler) 303 | } 304 | 305 | for (i in currentLowerLimit until currentUpperLimit) { 306 | if (i >= state.itemCount) continue 307 | val v = createView(i, recycler, state) 308 | if (v != null) { 309 | v.bringToFront() 310 | stackAlgorithm.transform( 311 | i.toFloat(), 312 | scroller.stackScroll, 313 | v 314 | ) 315 | } 316 | } 317 | 318 | } 319 | 320 | override fun checkLayoutParams(lp: RecyclerView.LayoutParams): Boolean { 321 | return lp is StackLayoutParams 322 | } 323 | 324 | override fun generateLayoutParams(lp: ViewGroup.LayoutParams): RecyclerView.LayoutParams { 325 | return StackLayoutParams(lp) 326 | } 327 | 328 | override fun generateLayoutParams( 329 | c: Context, 330 | attrs: AttributeSet 331 | ): RecyclerView.LayoutParams { 332 | return StackLayoutParams(c, attrs) 333 | } 334 | 335 | override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams { 336 | return StackLayoutParams(-2, -2) 337 | } 338 | 339 | class StackLayoutParams : RecyclerView.LayoutParams { 340 | constructor(width: Int, height: Int) : super(width, height) 341 | constructor(source: ViewGroup.LayoutParams?) : super(source) 342 | constructor(c: Context?, attrs: AttributeSet?) : 343 | super(c, attrs) 344 | 345 | var dimAmount: Float = 0f 346 | } 347 | 348 | internal inner class LayoutAlgorithm { 349 | var mMinScrollP = 0f 350 | var mMaxScrollP = 0f 351 | private var mInitialScrollP = 0f 352 | 353 | fun update(upperLimit: Int) { 354 | if (upperLimit == 0) { 355 | mInitialScrollP = 0f 356 | mMaxScrollP = mInitialScrollP 357 | mMinScrollP = mMaxScrollP 358 | return 359 | } 360 | val launchIndex = upperLimit - 1 361 | mMinScrollP = 0f 362 | mMaxScrollP = max( 363 | mMinScrollP, upperLimit - 1f 364 | ) 365 | mInitialScrollP = Internal.clamp( 366 | launchIndex.toFloat(), 367 | mMinScrollP, 368 | mMaxScrollP 369 | ) 370 | } 371 | 372 | 373 | private fun getLength(rect: Rect): Int { 374 | return if (!horizontalLayout) { 375 | rect.height() 376 | } else rect.width() 377 | } 378 | 379 | fun transform( 380 | position: Float, 381 | scroll: Float, 382 | view: View 383 | ) { 384 | tmpRect.setEmpty() 385 | 386 | //even though we should clamp the value between 0 and 1 387 | //it is better to leave it for the interpolator to handle 388 | //overshooting values 389 | val interpolatedValue = 390 | 1f - layoutInterpolator.getInterpolation( 391 | 1f - (position - scroll) 392 | ) 393 | 394 | 395 | val displacementFloat = 396 | interpolatedValue * getLength(marginsRect) * ( 397 | if (position < 1f && !centerFirstItem) 398 | getLength(marginsRect).toFloat() 399 | else 1f 400 | ) 401 | 402 | 403 | var displacement = displacementFloat.toInt() 404 | 405 | if (centerFirstItem && position < 1f && displacement > 0) 406 | displacement = 0 407 | 408 | tmpRect.set(viewRect) 409 | 410 | if (!horizontalLayout) { 411 | tmpRect.offset(0, displacement) 412 | } else { 413 | tmpRect.offset(displacement, 0) 414 | } 415 | 416 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 417 | view.setLeftTopRightBottom(tmpRect.left, tmpRect.top, tmpRect.right, tmpRect.bottom) 418 | } else { 419 | view.left = tmpRect.left 420 | view.top = tmpRect.top 421 | view.right = tmpRect.right 422 | view.bottom = tmpRect.bottom 423 | } 424 | 425 | view.getStackLayoutParams().dimAmount = 426 | if (scroll <= position) 0f 427 | else 428 | Internal.clamp01( 429 | Internal.DIMMING_INTERPOLATOR.getInterpolation(scroll - position) 430 | ) * 0.4f 431 | 432 | 433 | viewTransformer?.invoke(position - scroll, view, this@StackLayoutManager) 434 | } 435 | 436 | fun getDeltaPForY(dy: Float): Float { 437 | return dy / getLength(viewRect) * scrollMultiplier 438 | } 439 | 440 | fun getYForDeltaP(scroll: Float): Int { 441 | return (scroll * getLength(viewRect) * 442 | (1f / scrollMultiplier)).toInt() 443 | } 444 | } 445 | 446 | object ElevationTransformer { 447 | private fun mapRange(value: Float, min: Float, max: Float): Float { 448 | return min + value * (max - min) 449 | } 450 | 451 | private val minTranslationZ by lazy { 452 | TypedValue.applyDimension( 453 | TypedValue.COMPLEX_UNIT_DIP, 454 | 1f, 455 | Resources.getSystem().displayMetrics 456 | ) 457 | } 458 | private val maxTranslationZ by lazy { 459 | TypedValue.applyDimension( 460 | TypedValue.COMPLEX_UNIT_DIP, 461 | 10f, 462 | Resources.getSystem().displayMetrics 463 | ) 464 | } 465 | 466 | fun transform(x: Float, view: View, stackLayoutManager: StackLayoutManager) { 467 | if (Build.VERSION.SDK_INT < 21) return 468 | ViewCompat.setTranslationZ( 469 | view, mapRange( 470 | max( 471 | 0f, 472 | min( 473 | 1f, 474 | stackLayoutManager.layoutInterpolator.getInterpolation(x) 475 | ) 476 | ), 477 | minTranslationZ, 478 | maxTranslationZ 479 | ) 480 | ) 481 | } 482 | } 483 | 484 | internal inner class StackScroller { 485 | var stackScroll = 0f 486 | internal set 487 | 488 | fun boundScroll() { 489 | val curScroll = stackScroll 490 | val newScroll = getBoundedStackScroll(curScroll) 491 | if (newScroll.compareTo(curScroll) != 0) { 492 | stackScroll = newScroll 493 | } 494 | } 495 | 496 | private fun getBoundedStackScroll(scroll: Float): Float { 497 | return Internal.clamp( 498 | scroll, 499 | stackAlgorithm.mMinScrollP, 500 | stackAlgorithm.mMaxScrollP 501 | ) 502 | } 503 | 504 | private fun getScrollAmountOutOfBounds(scroll: Float): Float { 505 | if (scroll < stackAlgorithm.mMinScrollP) { 506 | return kotlin.math.abs(scroll - stackAlgorithm.mMinScrollP) 507 | } else if (scroll > stackAlgorithm.mMaxScrollP) { 508 | return kotlin.math.abs(scroll - stackAlgorithm.mMaxScrollP) 509 | } 510 | return 0f 511 | } 512 | 513 | val isScrollOutOfBounds: Boolean 514 | get() = getScrollAmountOutOfBounds(stackScroll).compareTo(0f) != 0 515 | 516 | } 517 | 518 | fun findFirstVisibleItemPosition(): Int { 519 | val child = findOneVisibleChild(0, childCount, false) 520 | return child?.let { getPosition(it) } ?: RecyclerView.NO_POSITION 521 | } 522 | 523 | fun findFirstCompletelyVisibleItemPosition(): Int { 524 | val child = findOneVisibleChild(0, childCount, true) 525 | return child?.let { getPosition(it) } ?: RecyclerView.NO_POSITION 526 | } 527 | 528 | fun findLastVisibleItemPosition(): Int { 529 | val child = findOneVisibleChild(childCount - 1, -1, false) 530 | return child?.let { getPosition(it) } ?: RecyclerView.NO_POSITION 531 | } 532 | 533 | fun findLastCompletelyVisibleItemPosition(): Int { 534 | val child = findOneVisibleChild(childCount - 1, -1, true) 535 | return child?.let { getPosition(it) } ?: RecyclerView.NO_POSITION 536 | } 537 | 538 | fun stopScrolling() { 539 | scrollAnimator?.also { scrollAnimator -> 540 | if (scrollAnimator.isRunning) { 541 | scrollAnimator.removeAllUpdateListeners() 542 | scrollAnimator.removeAllListeners() 543 | scrollAnimator.end() 544 | scrollAnimator.cancel() 545 | } 546 | } 547 | } 548 | 549 | companion object { 550 | private fun View.getStackLayoutParams(): StackLayoutParams { 551 | return this.layoutParams as StackLayoutParams 552 | } 553 | } 554 | 555 | private fun findOneVisibleChild( 556 | fromIndex: Int, 557 | toIndex: Int, 558 | completelyVisible: Boolean 559 | ): View? { 560 | val next = if (toIndex > fromIndex) 1 else -1 561 | var partiallyVisible: View? = null 562 | var i = fromIndex 563 | while (i != toIndex) { 564 | val child = getChildAt(i) 565 | if (child!!.top < height) { 566 | if (completelyVisible) { 567 | if (child.top < height && child.top + decoratedChildHeight < height) { 568 | return child 569 | } else if (!completelyVisible && partiallyVisible == null) { 570 | partiallyVisible = child 571 | } 572 | } else { 573 | return child 574 | } 575 | } 576 | i += next 577 | } 578 | return partiallyVisible 579 | } 580 | 581 | private fun createView( 582 | index: Int, 583 | recycler: Recycler, 584 | state: RecyclerView.State 585 | ): View? { 586 | if (state.itemCount == 0) return null 587 | val tv = recycler.getViewForPosition(index) 588 | if (tv.parent == null) { 589 | addView(tv, index % maxViews) 590 | layoutView(tv) 591 | } 592 | return tv 593 | } 594 | 595 | private fun layoutView(tv: View) { 596 | measureChildWithMargins(tv, 0, 0) 597 | tmpRect.setEmpty() 598 | if (tv.background != null) { 599 | tv.background.getPadding(tmpRect) 600 | } 601 | layoutDecoratedWithMargins( 602 | tv, 603 | viewRect.left - tmpRect.left, 604 | viewRect.top - tmpRect.top, 605 | viewRect.right + tmpRect.right, 606 | viewRect.bottom + tmpRect.bottom 607 | ) 608 | } 609 | 610 | fun peek() { 611 | stopScrolling() 612 | val out = 613 | animateScrollToItem(scroller.stackScroll + 0.6f) { 614 | val `in` = animateScrollToItem( 615 | scroller.stackScroll - 0.6f, 616 | stopScrollingRunnable 617 | ) 618 | `in`.interpolator = Internal.ACCELERATE_INTERPOLATOR 619 | `in`.duration = 400 620 | `in`.startDelay = 50 621 | `in`.start() 622 | } 623 | out.interpolator = Internal.SCROLL_INTERPOLATOR 624 | out.duration = 300 625 | out.start() 626 | } 627 | 628 | fun snap() { 629 | val toPosition = scroller.stackScroll.roundToInt().toFloat() 630 | animateScrollToItem(toPosition, null) 631 | .also { 632 | val delta = kotlin.math.abs(scroller.stackScroll - toPosition) 633 | it.duration = (500f * (1f - delta)).toLong() 634 | } 635 | .start() 636 | } 637 | 638 | internal object Internal { 639 | val ACCELERATE_INTERPOLATOR: TimeInterpolator = LogAccelerateInterpolator(60, 0) 640 | val SCROLL_INTERPOLATOR: TimeInterpolator = LogDecelerateInterpolator(60f, 0f) 641 | val LAYOUT_INTERPOLATOR: TimeInterpolator = LogDecelerateInterpolator(20f, 0f) 642 | val DIMMING_INTERPOLATOR: TimeInterpolator = LinearInterpolator() 643 | 644 | internal class LogDecelerateInterpolator( 645 | private val base: Float, 646 | private val drift: Float 647 | ) : 648 | TimeInterpolator { 649 | private fun computeLog(t: Float): Float { 650 | return 1f - base.toDouble().pow(-t.toDouble()).toFloat() + drift * t 651 | } 652 | 653 | override fun getInterpolation(t: Float): Float { 654 | return computeLog(t) / computeLog(1f) 655 | } 656 | 657 | } 658 | 659 | internal class LogAccelerateInterpolator(private val mBase: Int, private val mDrift: Int) : 660 | TimeInterpolator { 661 | private val mLogScale: Float = 1f / computeLog(1f, mBase, mDrift) 662 | override fun getInterpolation(t: Float): Float { 663 | return 1 - computeLog( 664 | 1 - t, 665 | mBase, 666 | mDrift 667 | ) * mLogScale 668 | } 669 | 670 | private fun computeLog(t: Float, base: Int, drift: Int): Float { 671 | return (-base.toDouble().pow(-t.toDouble())).toFloat() + 1 + drift * t 672 | } 673 | 674 | } 675 | 676 | fun clamp(value: Float, min: Float, max: Float): Float { 677 | return max(min, min(max, value)) 678 | } 679 | 680 | fun clamp01(value: Float): Float { 681 | return max(0f, min(1f, value)) 682 | } 683 | } 684 | } --------------------------------------------------------------------------------