├── .gitignore ├── LICENSE ├── README.md ├── animatedlist ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── animatedlist │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── animatedlist │ │ ├── AnimatedInfiniteList.kt │ │ ├── AnimatedInfiniteListImpl.kt │ │ ├── Color.kt │ │ ├── ListSizeSubcomposeLayout.kt │ │ ├── Util.kt │ │ └── model │ │ └── ListAnimationModel.kt │ └── test │ └── java │ └── com │ └── smarttoolfactory │ └── animatedlist │ └── ExampleUnitTest.kt ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── composeanimatedlist │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── composeanimatedlist │ │ │ ├── AspectRatioModel.kt │ │ │ ├── MainActivity.kt │ │ │ ├── PagerContent.kt │ │ │ ├── ShapeModel.kt │ │ │ ├── ShapeSelection.kt │ │ │ ├── Shapes.kt │ │ │ ├── Snack.kt │ │ │ ├── demo │ │ │ ├── AnimatedInfiniteListDemo.kt │ │ │ └── AnimatedInfiniteListDemo2.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── placeholder.jpg │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── smarttoolfactory │ └── composeanimatedlist │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/* 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Smart Tool Factory 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compose AnimatedList 2 | 3 | [![](https://jitpack.io/v/SmartToolFactory/Compose-AnimatedList.svg)](https://jitpack.io/#SmartToolFactory/Compose-AnimatedList) 4 | 5 | Animated infinite and finite LazyRow and LazyColumn with scale and color animations on scroll change 6 | based on how far they are to selector items 7 | 8 | https://user-images.githubusercontent.com/35650605/190077938-89d9f79a-06df-4052-b1fa-eee5651bd861.mp4 9 | 10 | ## Gradle Setup 11 | 12 | To get a Git project into your build: 13 | 14 | * Step 1. Add the JitPack repository to your build file Add it in your root build.gradle at the end 15 | of repositories: 16 | 17 | ``` 18 | allprojects { 19 | repositories { 20 | ... 21 | maven { url 'https://jitpack.io' } 22 | } 23 | } 24 | ``` 25 | 26 | * Step 2. Add the dependency 27 | 28 | ``` 29 | dependencies { 30 | implementation 'com.github.SmartToolFactory:Compose-AnimatedList:Tag' 31 | } 32 | ``` 33 | 34 | ## AnimatedInfiniteLazyRow/Column 35 | 36 | ### Declaration 37 | 38 | ```kotlin 39 | @Composable 40 | fun AnimatedInfiniteLazyColumn( 41 | modifier: Modifier = Modifier, 42 | items: List, 43 | initialFistVisibleIndex: Int = 0, 44 | visibleItemCount: Int = 5, 45 | inactiveItemPercent: Int = 85, 46 | spaceBetweenItems: Dp = 4.dp, 47 | selectorIndex: Int = -1, 48 | itemScaleRange: Int = 1, 49 | showPartialItem: Boolean = false, 50 | activeColor: Color = Color.Cyan, 51 | inactiveColor: Color = Color.Gray, 52 | key: ((index: Int) -> Any)? = null, 53 | contentType: (index: Int) -> Any? = { null }, 54 | itemContent: @Composable LazyItemScope.( 55 | animationProgress: AnimationProgress, 56 | index: Int, 57 | item: T, 58 | size: Dp, 59 | lazyListState: LazyListState 60 | ) -> Unit 61 | ) { 62 | AnimatedInfiniteList( 63 | modifier = modifier, 64 | items = items, 65 | initialFirstVisibleIndex = initialFistVisibleIndex, 66 | visibleItemCount = visibleItemCount, 67 | inactiveItemPercent = inactiveItemPercent, 68 | spaceBetweenItems = spaceBetweenItems, 69 | selectorIndex = selectorIndex, 70 | itemScaleRange = itemScaleRange, 71 | showPartialItem = showPartialItem, 72 | activeColor = activeColor, 73 | inactiveColor = inactiveColor, 74 | orientation = Orientation.Vertical, 75 | key = key, 76 | contentType = contentType, 77 | itemContent = itemContent 78 | ) 79 | } 80 | ``` 81 | 82 | ```kotlin 83 | @Composable 84 | fun AnimatedInfiniteLazyRow( 85 | modifier: Modifier = Modifier, 86 | items: List, 87 | initialFistVisibleIndex: Int = 0, 88 | activeItemWidth: Dp, 89 | inactiveItemWidth: Dp, 90 | visibleItemCount: Int = 5, 91 | spaceBetweenItems: Dp = 4.dp, 92 | selectorIndex: Int = -1, 93 | itemScaleRange: Int = 1, 94 | showPartialItem: Boolean = false, 95 | activeColor: Color = ActiveColor, 96 | inactiveColor: Color = InactiveColor, 97 | key: ((index: Int) -> Any)? = null, 98 | contentType: (index: Int) -> Any? = { null }, 99 | itemContent: @Composable LazyItemScope.( 100 | animationProgress: AnimationProgress, 101 | index: Int, 102 | item: T, 103 | size: Dp, 104 | lazyListState: LazyListState 105 | ) -> Unit 106 | ) 107 | ``` 108 | 109 | ### Params 110 | 111 | * **items** the data list 112 | * **visibleItemCount** count of items that are visible at any time 113 | * **inactiveItemPercent** percentage of scale that items inside **itemScaleRange** 114 | * can be scaled down to. This is a number between 0 and 100 115 | * **spaceBetweenItems** padding between 2 items 116 | * **selectorIndex** index of selector. When **itemScaleRange** is odd number it's center of selected 117 | item, when **itemScaleRange** is even number it's center of item with selector index and the one 118 | next to it 119 | * **itemScaleRange** range of area of scaling. When this value is odd any item that enters half of 120 | item size range subject to being scaled. When this value is even any item in 2 item size range is 121 | subject to being scaled 122 | * **showPartialItem** show items partially that are at the start and end 123 | * **activeColor** color of selected item 124 | * **inactiveColor** color of items are not selected 125 | * **key** a factory of stable and unique keys representing the item. Using the same key for multiple 126 | items in the list is not allowed. Type of the key should be savable via Bundle on Android. If 127 | null is passed the position in the list will represent the key. When you specify the key the 128 | scroll position will be maintained based on the key, which means if you add/remove items before 129 | the current visible item the item with the given key will be kept as the first visible one. 130 | * **contentType** a factory of the content types for the item. The item compositions of the same 131 | type could be reused more efficiently. Note that null is a valid type and items of such type will 132 | be considered compatible. 133 | * **itemContent** the content displayed by a single item 134 | 135 | ### Usage 136 | 137 | itemContent returns `AnimationProgress` which contains `LazyListState` and 138 | distance of each item to selector. By attaching a click modifier 139 | 140 | ```kotlin 141 | AnimatedInfiniteLazyRow( 142 | modifier = Modifier 143 | .width(listWidth) 144 | .padding(10.dp), 145 | items = aspectRatios, 146 | visibleItemCount = 5, 147 | activeItemWidth = 40.dp, 148 | inactiveItemWidth = 30.dp, 149 | selectorIndex = 1, 150 | spaceBetweenItems = 0.dp, 151 | inactiveColor = InactiveColor, 152 | activeColor = ActiveColor, 153 | itemContent = { animationProgress, index, item, width, lazyListState -> 154 | val color = animationProgress.color 155 | val scale = animationProgress.scale 156 | Box( 157 | modifier = Modifier 158 | .scale(scale) 159 | .background(color, CircleShape) 160 | .size(width) 161 | .clickable( 162 | interactionSource = remember { 163 | MutableInteractionSource() 164 | }, 165 | indication = null 166 | ) { 167 | coroutineScope.launch { 168 | lazyListState.animateScrollBy(animationProgress.distanceToSelector) 169 | } 170 | }, 171 | contentAlignment = Alignment.Center 172 | ) { 173 | 174 | Text( 175 | "$index", 176 | color = Color.White, 177 | fontSize = 20.sp, 178 | fontWeight = FontWeight.Bold 179 | ) 180 | } 181 | } 182 | ) 183 | ``` 184 | -------------------------------------------------------------------------------- /animatedlist/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /animatedlist/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.smarttoolfactory.animatedlist' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 21 12 | targetSdk 33 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 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_17 26 | targetCompatibility JavaVersion.VERSION_17 27 | } 28 | kotlinOptions { 29 | jvmTarget = '17' 30 | } 31 | buildFeatures { 32 | compose true 33 | } 34 | composeOptions { 35 | kotlinCompilerExtensionVersion = "1.4.7" 36 | } 37 | 38 | packagingOptions { 39 | resources { 40 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | 47 | implementation 'androidx.core:core-ktx:1.10.1' 48 | 49 | // Jetpack Compose 50 | implementation "androidx.compose.ui:ui:$compose_version" 51 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 52 | implementation "androidx.compose.material:material:$compose_version" 53 | implementation "androidx.compose.runtime:runtime:$compose_version" 54 | 55 | implementation "dev.chrisbanes.snapper:snapper:0.3.0" 56 | 57 | testImplementation 'junit:junit:4.13.2' 58 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 59 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 60 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 61 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 62 | } -------------------------------------------------------------------------------- /animatedlist/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/animatedlist/consumer-rules.pro -------------------------------------------------------------------------------- /animatedlist/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 -------------------------------------------------------------------------------- /animatedlist/src/androidTest/java/com/smarttoolfactory/animatedlist/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.animatedlist 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.smarttoolfactory.animatedlist.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /animatedlist/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /animatedlist/src/main/java/com/smarttoolfactory/animatedlist/AnimatedInfiniteList.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.animatedlist 2 | 3 | import androidx.compose.foundation.gestures.Orientation 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.foundation.lazy.LazyItemScope 6 | import androidx.compose.foundation.lazy.LazyListState 7 | import androidx.compose.foundation.lazy.LazyRow 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.unit.Dp 12 | import androidx.compose.ui.unit.dp 13 | import com.smarttoolfactory.animatedlist.model.AnimationProgress 14 | 15 | /** 16 | * [LazyColumn] with infinite items and color scale animation. When [showPartialItem] is 17 | * set to true items closest to start and end of the list are partially displayed 18 | * 19 | * @param items the data list 20 | * @param visibleItemCount count of items that are visible at any time 21 | * @param activeItemWidth width of selected item 22 | * @param inactiveItemWidth width of unselected items 23 | * @param spaceBetweenItems padding between 2 items 24 | * @param selectorIndex index of selector. When [itemScaleRange] is odd number it's center of 25 | * selected item, when [itemScaleRange] is even number it's center of item with selector index 26 | * and the one next to it 27 | * @param itemScaleRange range of area of scaling. When this value is odd 28 | * any item that enters half of item size range subject to being scaled. When this value is even 29 | * any item in 2 item size range is subject to being scaled 30 | * @param showPartialItem show items partially that are at the start and end 31 | * @param activeColor color of selected item 32 | * @param inactiveColor color of items are not selected 33 | * @param key a factory of stable and unique keys representing the item. Using the same key 34 | * for multiple items in the list is not allowed. Type of the key should be saveable 35 | * via Bundle on Android. If null is passed the position in the list will represent the key. 36 | * When you specify the key the scroll position will be maintained based on the key, which 37 | * means if you add/remove items before the current visible item the item with the given key 38 | * will be kept as the first visible one. 39 | * @param contentType a factory of the content types for the item. The item compositions of 40 | * the same type could be reused more efficiently. Note that null is a valid type and items of such 41 | * type will be considered compatible. 42 | * @param itemContent the content displayed by a single item 43 | */ 44 | @Composable 45 | fun AnimatedInfiniteLazyRow( 46 | modifier: Modifier = Modifier, 47 | items: List, 48 | initialFistVisibleIndex: Int = 0, 49 | activeItemWidth: Dp, 50 | inactiveItemWidth: Dp, 51 | visibleItemCount: Int = 5, 52 | spaceBetweenItems: Dp = 4.dp, 53 | selectorIndex: Int = -1, 54 | itemScaleRange: Int = 1, 55 | showPartialItem: Boolean = false, 56 | activeColor: Color = ActiveColor, 57 | inactiveColor: Color = InactiveColor, 58 | key: ((index: Int) -> Any)? = null, 59 | contentType: (index: Int) -> Any? = { null }, 60 | itemContent: @Composable LazyItemScope.( 61 | animationProgress: AnimationProgress, 62 | index: Int, 63 | item: T, 64 | size: Dp, 65 | lazyListState: LazyListState 66 | ) -> Unit 67 | ) { 68 | AnimatedInfiniteList( 69 | modifier = modifier, 70 | items = items, 71 | initialFirstVisibleIndex = initialFistVisibleIndex, 72 | visibleItemCount = visibleItemCount, 73 | activeItemSize = activeItemWidth, 74 | inactiveItemSize = inactiveItemWidth, 75 | spaceBetweenItems = spaceBetweenItems, 76 | selectorIndex = selectorIndex, 77 | itemScaleRange = itemScaleRange, 78 | showPartialItem = showPartialItem, 79 | activeColor = activeColor, 80 | inactiveColor = inactiveColor, 81 | key = key, 82 | contentType = contentType, 83 | itemContent = itemContent 84 | ) 85 | } 86 | 87 | /** 88 | * [LazyRow] with infinite items and color scale animation. When [showPartialItem] is 89 | * set to true items closest to start and end of the list are partially displayed 90 | * 91 | * @param items the data list 92 | * @param visibleItemCount count of items that are visible at any time 93 | * @param activeItemHeight height of selected item 94 | * @param inactiveItemHeight height of unselected items 95 | * @param spaceBetweenItems padding between 2 items 96 | * @param selectorIndex index of selector. When [itemScaleRange] is odd number it's center of 97 | * selected item, when [itemScaleRange] is even number it's center of item with selector index 98 | * and the one next to it 99 | * @param itemScaleRange range of area of scaling. When this value is odd 100 | * any item that enters half of item size range subject to being scaled. When this value is even 101 | * any item in 2 item size range is subject to being scaled 102 | * @param showPartialItem show items partially that are at the start and end 103 | * @param activeColor color of selected item 104 | * @param inactiveColor color of items are not selected 105 | * @param key a factory of stable and unique keys representing the item. Using the same key 106 | * for multiple items in the list is not allowed. Type of the key should be saveable 107 | * via Bundle on Android. If null is passed the position in the list will represent the key. 108 | * When you specify the key the scroll position will be maintained based on the key, which 109 | * means if you add/remove items before the current visible item the item with the given key 110 | * will be kept as the first visible one. 111 | * @param contentType a factory of the content types for the item. The item compositions of 112 | * the same type could be reused more efficiently. Note that null is a valid type and items of such 113 | * type will be considered compatible. 114 | * @param itemContent the content displayed by a single item 115 | */ 116 | @Composable 117 | fun AnimatedInfiniteLazyColumn( 118 | modifier: Modifier = Modifier, 119 | items: List, 120 | initialFirstVisibleIndex: Int = 0, 121 | visibleItemCount: Int = 5, 122 | activeItemHeight: Dp, 123 | inactiveItemHeight: Dp, 124 | spaceBetweenItems: Dp = 4.dp, 125 | selectorIndex: Int = -1, 126 | itemScaleRange: Int = 1, 127 | showPartialItem: Boolean = false, 128 | activeColor: Color = ActiveColor, 129 | inactiveColor: Color = InactiveColor, 130 | key: ((index: Int) -> Any)? = null, 131 | contentType: (index: Int) -> Any? = { null }, 132 | itemContent: @Composable LazyItemScope.( 133 | animationProgress: AnimationProgress, 134 | index: Int, 135 | item: T, 136 | size: Dp, 137 | lazyListState: LazyListState 138 | ) -> Unit 139 | ) { 140 | AnimatedInfiniteList( 141 | modifier = modifier, 142 | items = items, 143 | initialFirstVisibleIndex = initialFirstVisibleIndex, 144 | visibleItemCount = visibleItemCount, 145 | activeItemSize = activeItemHeight, 146 | inactiveItemSize = inactiveItemHeight, 147 | spaceBetweenItems = spaceBetweenItems, 148 | selectorIndex = selectorIndex, 149 | itemScaleRange = itemScaleRange, 150 | showPartialItem = showPartialItem, 151 | activeColor = activeColor, 152 | inactiveColor = inactiveColor, 153 | orientation = Orientation.Vertical, 154 | key = key, 155 | contentType = contentType, 156 | itemContent = itemContent 157 | ) 158 | } 159 | 160 | /** 161 | * [LazyRow] with infinite items and color scale animation. When [showPartialItem] is 162 | * set to true items closest to start and end of the list are partially displayed 163 | * 164 | * @param items the data list 165 | * @param visibleItemCount count of items that are visible at any time 166 | * @param inactiveItemPercent percentage of scale that items inside [itemScaleRange] 167 | * can be scaled down to. This is a number between 0 and 100 168 | * @param spaceBetweenItems padding between 2 items 169 | * @param selectorIndex index of selector. When [itemScaleRange] is odd number it's center of 170 | * selected item, when [itemScaleRange] is even number it's center of item with selector index 171 | * and the one next to it 172 | * @param itemScaleRange range of area of scaling. When this value is odd 173 | * any item that enters half of item size range subject to being scaled. When this value is even 174 | * any item in 2 item size range is subject to being scaled 175 | * @param showPartialItem show items partially that are at the start and end 176 | * @param activeColor color of selected item 177 | * @param inactiveColor color of items are not selected 178 | * @param key a factory of stable and unique keys representing the item. Using the same key 179 | * for multiple items in the list is not allowed. Type of the key should be saveable 180 | * via Bundle on Android. If null is passed the position in the list will represent the key. 181 | * When you specify the key the scroll position will be maintained based on the key, which 182 | * means if you add/remove items before the current visible item the item with the given key 183 | * will be kept as the first visible one. 184 | * @param contentType a factory of the content types for the item. The item compositions of 185 | * the same type could be reused more efficiently. Note that null is a valid type and items of such 186 | * type will be considered compatible. 187 | * @param itemContent the content displayed by a single item 188 | */ 189 | @Composable 190 | fun AnimatedInfiniteLazyRow( 191 | modifier: Modifier = Modifier, 192 | items: List, 193 | initialFirstVisibleIndex: Int = 0, 194 | visibleItemCount: Int = 5, 195 | inactiveItemPercent: Int = 85, 196 | spaceBetweenItems: Dp = 16.dp, 197 | selectorIndex: Int = -1, 198 | itemScaleRange: Int = 1, 199 | showPartialItem: Boolean = false, 200 | activeColor: Color = ActiveColor, 201 | inactiveColor: Color = InactiveColor, 202 | key: ((index: Int) -> Any)? = null, 203 | contentType: (index: Int) -> Any? = { null }, 204 | itemContent: @Composable LazyItemScope.( 205 | animationProgress: AnimationProgress, 206 | index: Int, 207 | item: T, 208 | size: Dp, 209 | lazyListState: LazyListState 210 | ) -> Unit 211 | ) { 212 | AnimatedInfiniteList( 213 | modifier = modifier, 214 | items = items, 215 | initialFirstVisibleIndex = initialFirstVisibleIndex, 216 | visibleItemCount = visibleItemCount, 217 | inactiveItemPercent = inactiveItemPercent, 218 | spaceBetweenItems = spaceBetweenItems, 219 | selectorIndex = selectorIndex, 220 | itemScaleRange = itemScaleRange, 221 | showPartialItem = showPartialItem, 222 | activeColor = activeColor, 223 | inactiveColor = inactiveColor, 224 | key = key, 225 | contentType = contentType, 226 | itemContent = itemContent 227 | ) 228 | } 229 | 230 | /** 231 | * [LazyColumn] with infinite items and color scale animation. When [showPartialItem] is 232 | * set to true items closest to start and end of the list are partially displayed 233 | * 234 | * @param items the data list 235 | * @param visibleItemCount count of items that are visible at any time 236 | * @param inactiveItemPercent percentage of scale that items inside [itemScaleRange] 237 | * can be scaled down to. This is a number between 0 and 100 238 | * @param spaceBetweenItems padding between 2 items 239 | * @param selectorIndex index of selector. When [itemScaleRange] is odd number it's center of 240 | * selected item, when [itemScaleRange] is even number it's center of item with selector index 241 | * and the one next to it 242 | * @param itemScaleRange range of area of scaling. When this value is odd 243 | * any item that enters half of item size range subject to being scaled. When this value is even 244 | * any item in 2 item size range is subject to being scaled 245 | * @param showPartialItem show items partially that are at the start and end 246 | * @param activeColor color of selected item 247 | * @param inactiveColor color of items are not selected 248 | * @param key a factory of stable and unique keys representing the item. Using the same key 249 | * for multiple items in the list is not allowed. Type of the key should be saveable 250 | * via Bundle on Android. If null is passed the position in the list will represent the key. 251 | * When you specify the key the scroll position will be maintained based on the key, which 252 | * means if you add/remove items before the current visible item the item with the given key 253 | * will be kept as the first visible one. 254 | * @param contentType a factory of the content types for the item. The item compositions of 255 | * the same type could be reused more efficiently. Note that null is a valid type and items of such 256 | * type will be considered compatible. 257 | * @param itemContent the content displayed by a single item 258 | */ 259 | @Composable 260 | fun AnimatedInfiniteLazyColumn( 261 | modifier: Modifier = Modifier, 262 | items: List, 263 | initialFistVisibleIndex: Int = 0, 264 | visibleItemCount: Int = 5, 265 | inactiveItemPercent: Int = 85, 266 | spaceBetweenItems: Dp = 4.dp, 267 | selectorIndex: Int = -1, 268 | itemScaleRange: Int = 1, 269 | showPartialItem: Boolean = false, 270 | activeColor: Color = Color.Cyan, 271 | inactiveColor: Color = Color.Gray, 272 | key: ((index: Int) -> Any)? = null, 273 | contentType: (index: Int) -> Any? = { null }, 274 | itemContent: @Composable LazyItemScope.( 275 | animationProgress: AnimationProgress, 276 | index: Int, 277 | item: T, 278 | size: Dp, 279 | lazyListState: LazyListState 280 | ) -> Unit 281 | ) { 282 | AnimatedInfiniteList( 283 | modifier = modifier, 284 | items = items, 285 | initialFirstVisibleIndex = initialFistVisibleIndex, 286 | visibleItemCount = visibleItemCount, 287 | inactiveItemPercent = inactiveItemPercent, 288 | spaceBetweenItems = spaceBetweenItems, 289 | selectorIndex = selectorIndex, 290 | itemScaleRange = itemScaleRange, 291 | showPartialItem = showPartialItem, 292 | activeColor = activeColor, 293 | inactiveColor = inactiveColor, 294 | orientation = Orientation.Vertical, 295 | key = key, 296 | contentType = contentType, 297 | itemContent = itemContent 298 | ) 299 | } -------------------------------------------------------------------------------- /animatedlist/src/main/java/com/smarttoolfactory/animatedlist/AnimatedInfiniteListImpl.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSnapperApi::class) 2 | 3 | package com.smarttoolfactory.animatedlist 4 | 5 | import androidx.compose.foundation.gestures.FlingBehavior 6 | import androidx.compose.foundation.gestures.Orientation 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.fillMaxHeight 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.foundation.lazy.LazyColumn 14 | import androidx.compose.foundation.lazy.LazyItemScope 15 | import androidx.compose.foundation.lazy.LazyListItemInfo 16 | import androidx.compose.foundation.lazy.LazyListLayoutInfo 17 | import androidx.compose.foundation.lazy.LazyListScope 18 | import androidx.compose.foundation.lazy.LazyListState 19 | import androidx.compose.foundation.lazy.LazyRow 20 | import androidx.compose.foundation.lazy.rememberLazyListState 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.derivedStateOf 23 | import androidx.compose.runtime.getValue 24 | import androidx.compose.runtime.mutableStateOf 25 | import androidx.compose.runtime.remember 26 | import androidx.compose.runtime.setValue 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.graphics.lerp 31 | import androidx.compose.ui.platform.LocalDensity 32 | import androidx.compose.ui.unit.Dp 33 | import androidx.compose.ui.unit.dp 34 | import com.smarttoolfactory.animatedlist.model.AnimationProgress 35 | import dev.chrisbanes.snapper.ExperimentalSnapperApi 36 | import dev.chrisbanes.snapper.SnapOffsets 37 | import dev.chrisbanes.snapper.SnapperLayoutInfo 38 | import dev.chrisbanes.snapper.SnapperLayoutItemInfo 39 | import dev.chrisbanes.snapper.rememberSnapperFlingBehavior 40 | import kotlin.math.absoluteValue 41 | 42 | /** 43 | * Infinite list with color and scale animation for selecting aspect ratio 44 | * 45 | * @param items the data list 46 | * @param initialFirstVisibleIndex first visible item of the list. This can be 47 | * negative number too. It's in range of [items]. If there are 10 items -2th item is 48 | * actually 8th item is the first visible item on screen 49 | * @param visibleItemCount count of items that are visible at any time 50 | * @param activeItemSize width or height of selected item 51 | * @param inactiveItemSize width or height of unselected items 52 | * @param spaceBetweenItems padding between 2 items 53 | * @param selectorIndex index of selector. When [itemScaleRange] is odd number it's center of 54 | * selected item, when [itemScaleRange] is even number it's center of item with selector index 55 | * and the one next to it 56 | * @param itemScaleRange range of area of scaling. When this value is odd 57 | * any item that enters half of item size range subject to being scaled. When this value is even 58 | * any item in 2 item size range is subject to being scaled 59 | * @param showPartialItem show items partially that are at the start and end 60 | * @param activeColor color of selected item 61 | * @param inactiveColor color of items are not selected 62 | * @param key a factory of stable and unique keys representing the item. Using the same key 63 | * for multiple items in the list is not allowed. Type of the key should be saveable 64 | * via Bundle on Android. If null is passed the position in the list will represent the key. 65 | * When you specify the key the scroll position will be maintained based on the key, which 66 | * means if you add/remove items before the current visible item the item with the given key 67 | * will be kept as the first visible one. 68 | * @param contentType a factory of the content types for the item. The item compositions of 69 | * the same type could be reused more efficiently. Note that null is a valid type and items of such 70 | * type will be considered compatible. 71 | * @param itemContent the content displayed by a single item 72 | */ 73 | @OptIn(ExperimentalSnapperApi::class) 74 | @Composable 75 | internal fun AnimatedInfiniteList( 76 | modifier: Modifier = Modifier, 77 | items: List, 78 | initialFirstVisibleIndex: Int = 0, 79 | visibleItemCount: Int = 5, 80 | activeItemSize: Dp, 81 | inactiveItemSize: Dp, 82 | spaceBetweenItems: Dp = 16.dp, 83 | selectorIndex: Int = -1, 84 | itemScaleRange: Int = 1, 85 | showPartialItem: Boolean = false, 86 | activeColor: Color = ActiveColor, 87 | inactiveColor: Color = InactiveColor, 88 | orientation: Orientation = Orientation.Horizontal, 89 | key: ((index: Int) -> Any)? = null, 90 | contentType: (index: Int) -> Any? = { null }, 91 | itemContent: @Composable LazyItemScope.( 92 | animationProgress: AnimationProgress, 93 | index: Int, 94 | item: T, 95 | size: Dp, 96 | lazyListState: LazyListState 97 | ) -> Unit 98 | ) { 99 | val inactiveItemScale = inactiveItemSize.value / activeItemSize.value 100 | 101 | val listDimension = 102 | activeItemSize * visibleItemCount + spaceBetweenItems * (visibleItemCount - 1) 103 | 104 | val listModifier = if (orientation == Orientation.Horizontal) { 105 | Modifier.width(listDimension) 106 | } else { 107 | Modifier.height(listDimension) 108 | } 109 | 110 | val availableSpace = LocalDensity.current.run { listDimension.toPx() } 111 | 112 | val itemSize = LocalDensity.current.run { activeItemSize.toPx() } 113 | 114 | // number of items 115 | val totalItemCount = items.size 116 | 117 | // Number of items that are visible 118 | val oddNumberOfVisibleItems = visibleItemCount % 2 == 1 119 | 120 | // Index of selector(item that is selected) in circular list 121 | val indexOfSelector = (if (selectorIndex == -1) { 122 | if (oddNumberOfVisibleItems && !showPartialItem) { 123 | visibleItemCount / 2 124 | } else { 125 | visibleItemCount / 2 - 1 126 | } 127 | } else selectorIndex) 128 | .coerceIn(0, visibleItemCount - 1) 129 | 130 | val snapOffsetForItem = if (oddNumberOfVisibleItems && showPartialItem) { 131 | { _: SnapperLayoutInfo, _: SnapperLayoutItemInfo -> 132 | (itemSize / 2).toInt() 133 | } 134 | } else if (!oddNumberOfVisibleItems && !showPartialItem) { 135 | SnapOffsets.Start 136 | } else { 137 | SnapOffsets.Center 138 | } 139 | 140 | val centerItem = (Int.MAX_VALUE / 2) 141 | val initialVisibleGlobalIndex = 142 | centerItem - centerItem % totalItemCount + initialFirstVisibleIndex 143 | 144 | val listState = rememberLazyListState( 145 | initialFirstVisibleItemIndex = initialVisibleGlobalIndex, 146 | initialFirstVisibleItemScrollOffset = if (showPartialItem) (itemSize / 2).toInt() else 0 147 | ) 148 | 149 | val flingBehavior = rememberSnapperFlingBehavior( 150 | lazyListState = listState, 151 | snapOffsetForItem = snapOffsetForItem 152 | ) 153 | 154 | Box( 155 | modifier = modifier, 156 | contentAlignment = Alignment.Center 157 | ) { 158 | AnimatedCircularListImpl( 159 | modifier = listModifier, 160 | items = items, 161 | lazyListState = listState, 162 | flingBehavior = flingBehavior, 163 | initialFirstVisibleIndex = initialVisibleGlobalIndex, 164 | visibleItemCount = visibleItemCount, 165 | availableSpace = availableSpace, 166 | itemSize = activeItemSize, 167 | spaceBetweenItems = spaceBetweenItems, 168 | totalItemCount = totalItemCount, 169 | indexOfSelector = indexOfSelector, 170 | itemScaleRange = itemScaleRange, 171 | showPartialItem = showPartialItem, 172 | activeColor = activeColor, 173 | inactiveColor = inactiveColor, 174 | inactiveItemFraction = inactiveItemScale, 175 | orientation = orientation, 176 | key = key, 177 | contentType = contentType, 178 | itemContent = itemContent 179 | ) 180 | } 181 | } 182 | 183 | /** 184 | * Infinite list with color and scale animation 185 | * 186 | * @param items the data list 187 | * @param visibleItemCount count of items that are visible at any time 188 | * @param inactiveItemPercent percentage of scale that items inside [itemScaleRange] 189 | * can be scaled down to. This is a number between 0 and 100 190 | * @param spaceBetweenItems padding between 2 items 191 | * @param selectorIndex index of selector. When [itemScaleRange] is odd number it's center of 192 | * selected item, when [itemScaleRange] is even number it's center of item with selecter index 193 | * and the one next to it 194 | * @param itemScaleRange range of area of scaling. When this value is odd 195 | * any item that enters half of item size range subject to being scaled. When this value is even 196 | * any item in 2 item size range is subject to being scaled 197 | * @param showPartialItem show items partially that are at the start and end 198 | * @param activeColor color of selected item 199 | * @param inactiveColor color of items are not selected 200 | * @param key a factory of stable and unique keys representing the item. Using the same key 201 | * for multiple items in the list is not allowed. Type of the key should be saveable 202 | * via Bundle on Android. If null is passed the position in the list will represent the key. 203 | * When you specify the key the scroll position will be maintained based on the key, which 204 | * means if you add/remove items before the current visible item the item with the given key 205 | * will be kept as the first visible one. 206 | * @param contentType a factory of the content types for the item. The item compositions of 207 | * the same type could be reused more efficiently. Note that null is a valid type and items of such 208 | * type will be considered compatible. 209 | * @param itemContent the content displayed by a single item 210 | */ 211 | @Composable 212 | internal fun AnimatedInfiniteList( 213 | modifier: Modifier = Modifier, 214 | items: List, 215 | initialFirstVisibleIndex: Int = 0, 216 | visibleItemCount: Int = 5, 217 | inactiveItemPercent: Int = 85, 218 | spaceBetweenItems: Dp = 16.dp, 219 | selectorIndex: Int = -1, 220 | itemScaleRange: Int = 1, 221 | showPartialItem: Boolean = false, 222 | activeColor: Color = ActiveColor, 223 | inactiveColor: Color = InactiveColor, 224 | orientation: Orientation = Orientation.Horizontal, 225 | key: ((index: Int) -> Any)? = null, 226 | contentType: (index: Int) -> Any? = { null }, 227 | itemContent: @Composable LazyItemScope.( 228 | animationProgress: AnimationProgress, 229 | index: Int, 230 | item: T, 231 | size: Dp, 232 | lazyListState: LazyListState 233 | ) -> Unit 234 | ) { 235 | 236 | val listModifier = if (orientation == Orientation.Horizontal) { 237 | modifier.fillMaxWidth() 238 | } else { 239 | modifier.fillMaxHeight() 240 | } 241 | 242 | ListSizeSubcomposeLayout( 243 | modifier = listModifier, 244 | mainContent = { 245 | Box(modifier = listModifier) 246 | } 247 | ) { 248 | val width = it.width 249 | val height = it.height 250 | 251 | val availableSpace = if (orientation == Orientation.Horizontal) width else height 252 | val density = LocalDensity.current 253 | val spaceBetweenItemsPx = density.run { spaceBetweenItems.toPx() } 254 | 255 | val itemSize = 256 | (availableSpace - spaceBetweenItemsPx * (visibleItemCount - 1)) / visibleItemCount 257 | val itemSizeDp = density.run { itemSize.toDp() } 258 | val inActiveItemSizeDp = density.run { (itemSize * inactiveItemPercent / 100).toDp() } 259 | val widthDp = density.run { width.toDp() } 260 | val heightDp = density.run { height.toDp() } 261 | 262 | AnimatedInfiniteList( 263 | modifier = if (orientation == Orientation.Horizontal) Modifier.width(widthDp) 264 | else Modifier.height(heightDp), 265 | items = items, 266 | initialFirstVisibleIndex = initialFirstVisibleIndex, 267 | visibleItemCount = visibleItemCount, 268 | activeItemSize = itemSizeDp, 269 | inactiveItemSize = inActiveItemSizeDp, 270 | spaceBetweenItems = spaceBetweenItems, 271 | selectorIndex = selectorIndex, 272 | itemScaleRange = itemScaleRange, 273 | showPartialItem = showPartialItem, 274 | activeColor = activeColor, 275 | inactiveColor = inactiveColor, 276 | orientation = orientation, 277 | key = key, 278 | contentType = contentType, 279 | itemContent = itemContent 280 | ) 281 | } 282 | } 283 | 284 | @Composable 285 | private fun AnimatedCircularListImpl( 286 | modifier: Modifier, 287 | items: List, 288 | lazyListState: LazyListState, 289 | flingBehavior: FlingBehavior, 290 | initialFirstVisibleIndex: Int, 291 | visibleItemCount: Int, 292 | availableSpace: Float, 293 | itemSize: Dp, 294 | spaceBetweenItems: Dp, 295 | totalItemCount: Int, 296 | indexOfSelector: Int, 297 | itemScaleRange: Int, 298 | showPartialItem: Boolean, 299 | activeColor: Color, 300 | inactiveColor: Color, 301 | inactiveItemFraction: Float, 302 | orientation: Orientation, 303 | key: ((index: Int) -> Any)?, 304 | contentType: (index: Int) -> Any?, 305 | itemContent: @Composable LazyItemScope.( 306 | animationProgress: AnimationProgress, 307 | index: Int, 308 | item: T, 309 | size: Dp, 310 | lazyListState: LazyListState 311 | ) -> Unit 312 | ) { 313 | 314 | val density = LocalDensity.current 315 | val spaceBetweenItemsPx = density.run { spaceBetweenItems.toPx() } 316 | 317 | val content: LazyListScope.() -> Unit = { 318 | items( 319 | count = Int.MAX_VALUE, key = key, contentType = contentType 320 | ) { globalIndex -> 321 | AnimatedItems( 322 | lazyListState = lazyListState, 323 | initialFirstVisibleIndex = initialFirstVisibleIndex, 324 | indexOfSelector = indexOfSelector, 325 | itemScaleRange = itemScaleRange, 326 | showPartialItem = showPartialItem, 327 | globalIndex = globalIndex, 328 | availableSpace = availableSpace, 329 | itemSize = itemSize, 330 | spaceBetweenItems = spaceBetweenItemsPx, 331 | visibleItemCount = visibleItemCount, 332 | totalItemCount = totalItemCount, 333 | inactiveItemScale = inactiveItemFraction, 334 | inactiveColor = inactiveColor, 335 | activeColor = activeColor 336 | ) { animationProgress: AnimationProgress, size: Dp -> 337 | val localIndex = globalIndex % totalItemCount 338 | itemContent( 339 | animationProgress, 340 | localIndex, 341 | items[localIndex], 342 | size, 343 | lazyListState 344 | ) 345 | } 346 | } 347 | } 348 | 349 | if (orientation == Orientation.Horizontal) { 350 | LazyRow( 351 | modifier = modifier, 352 | state = lazyListState, 353 | horizontalArrangement = Arrangement.spacedBy(spaceBetweenItems), 354 | flingBehavior = flingBehavior 355 | ) { 356 | content() 357 | } 358 | } else { 359 | LazyColumn( 360 | modifier = modifier, 361 | state = lazyListState, 362 | verticalArrangement = Arrangement.spacedBy(spaceBetweenItems), 363 | flingBehavior = flingBehavior 364 | ) { 365 | content() 366 | } 367 | } 368 | } 369 | 370 | @Composable 371 | private fun LazyItemScope.AnimatedItems( 372 | lazyListState: LazyListState, 373 | initialFirstVisibleIndex: Int, 374 | indexOfSelector: Int, 375 | itemScaleRange: Int, 376 | showPartialItem: Boolean, 377 | globalIndex: Int, 378 | availableSpace: Float, 379 | itemSize: Dp, 380 | spaceBetweenItems: Float, 381 | visibleItemCount: Int, 382 | totalItemCount: Int, 383 | inactiveItemScale: Float, 384 | inactiveColor: Color, 385 | activeColor: Color, 386 | itemContent: @Composable LazyItemScope.(animationProgress: AnimationProgress, size: Dp) -> Unit 387 | 388 | ) { 389 | var selectedIndex by remember { 390 | mutableStateOf(-1) 391 | } 392 | 393 | val itemSizePx = LocalDensity.current.run { itemSize.toPx() } 394 | 395 | val animationData by remember( 396 | indexOfSelector, 397 | visibleItemCount, 398 | inactiveItemScale, 399 | itemScaleRange, 400 | showPartialItem 401 | ) { 402 | derivedStateOf { 403 | val animationData = getAnimationProgress( 404 | lazyListState = lazyListState, 405 | initialFirstVisibleIndex = initialFirstVisibleIndex, 406 | indexOfSelector = indexOfSelector, 407 | itemScaleRange = itemScaleRange, 408 | showPartialItem = showPartialItem, 409 | globalIndex = globalIndex, 410 | selectedIndex = selectedIndex, 411 | availableSpace = availableSpace, 412 | itemSize = itemSizePx, 413 | spaceBetweenItems = spaceBetweenItems, 414 | visibleItemCount = visibleItemCount, 415 | totalItemCount = totalItemCount, 416 | inactiveScale = inactiveItemScale, 417 | inactiveColor = inactiveColor, 418 | activeColor = activeColor 419 | ) 420 | 421 | selectedIndex = animationData.globalItemIndex 422 | animationData 423 | } 424 | } 425 | itemContent(animationData, itemSize) 426 | } 427 | 428 | /** 429 | * get color, scale and selected index for scroll progress for infinite or circular list with 430 | * [Int.MAX_VALUE] global index count 431 | * 432 | * @param lazyListState A state object that can be hoisted to control and observe scrolling 433 | * @param initialFirstVisibleIndex index of item that is at the beginning of the list initially 434 | * @param indexOfSelector global index of element of selector of infinite items. Item with 435 | * this index is selected item 436 | * @param globalIndex index of current item. This index changes for every item in list 437 | * that calls this function 438 | * @param selectedIndex global index of currently selected item. This index changes only 439 | * when selected item is changed due to scroll 440 | * @param availableSpace space that is reserved for items and space between items. This 441 | * param is list width/height minus padding values in respective axis. 442 | * @param itemSize width/height of each item 443 | * @param spaceBetweenItems space between each item 444 | * @param visibleItemCount count of visible items on screen 445 | * @param totalItemCount count of items that are displayed in circular list 446 | * @param inactiveScale lower scale that items can be scaled to. It should be less than 1f 447 | * @param inactiveColor color of items when they are not selected 448 | * @param activeColor color of item that is selected 449 | */ 450 | private fun getAnimationProgress( 451 | lazyListState: LazyListState, 452 | initialFirstVisibleIndex: Int, 453 | indexOfSelector: Int, 454 | itemScaleRange: Int, 455 | showPartialItem: Boolean, 456 | globalIndex: Int, 457 | selectedIndex: Int, 458 | availableSpace: Float, 459 | itemSize: Float, 460 | spaceBetweenItems: Float, 461 | visibleItemCount: Int, 462 | totalItemCount: Int, 463 | inactiveScale: Float, 464 | inactiveColor: Color, 465 | activeColor: Color, 466 | ): AnimationProgress { 467 | 468 | val visibleItems = lazyListState.layoutInfo.visibleItemsInfo 469 | val currentItem: LazyListItemInfo? = visibleItems.firstOrNull { it.index == globalIndex } 470 | val halfItemSize = itemSize / 2 471 | 472 | var isRangeOfSelectionOdd = itemScaleRange % 2 == 1 473 | // If partial item is shown range 474 | if (showPartialItem) isRangeOfSelectionOdd = !isRangeOfSelectionOdd 475 | 476 | // Position of selector item 477 | val selectorPosition = 478 | getSelectorPosition(isRangeOfSelectionOdd, indexOfSelector, itemSize, spaceBetweenItems) 479 | 480 | // Get offset of each item relative to start of list x=0 or y=0 position 481 | val itemCenter = (halfItemSize).toInt() + if (currentItem != null) { 482 | currentItem.offset 483 | } else { 484 | // Convert global indexes to indexes in range of 0..visibleItemCount 485 | // when current item is null on initial run 486 | val localIndex = 487 | (visibleItemCount + globalIndex - initialFirstVisibleIndex) % visibleItemCount 488 | var initialItemCenter = (localIndex * itemSize + localIndex * spaceBetweenItems) 489 | // If we show partial items offset from 0 by half of items size to right or up 490 | if (showPartialItem) initialItemCenter -= halfItemSize 491 | initialItemCenter.toInt() 492 | } 493 | 494 | // get scale of current item based on distance between items center to selector 495 | val scale = getScale( 496 | rangeOfSelection = itemScaleRange, 497 | selectorPosition = selectorPosition, 498 | itemCenter = itemCenter, 499 | inactiveScale = inactiveScale, 500 | itemSize = itemSize, 501 | spaceBetweenItems = spaceBetweenItems 502 | ).coerceIn(0f, 1f) 503 | 504 | val globalSelectedIndex = 505 | getIndexClosestToSelector(selectedIndex, halfItemSize, selectorPosition, visibleItems) 506 | 507 | 508 | // Index of item in list. If list has 7 items initial item index is 3 509 | // When selector changes we get what it(in infinite list) corresponds to in item list 510 | val itemIndex = if (globalSelectedIndex > 0) { 511 | globalSelectedIndex % totalItemCount 512 | } else { 513 | indexOfSelector 514 | } 515 | 516 | // This is the fraction between lower bound and 1f. If lower bound is .9f we have 517 | // range of 0.9f..1f for scale calculation 518 | val scalingInterval = 1f - inactiveScale 519 | 520 | // Scale for color when scale is at lower bound color scale is zero 521 | // when scale reaches upper bound(1f) color scale is 1f which is target color 522 | // when argEvaluator evaluates color 523 | val colorScale = if (scalingInterval == 0f) 1f 524 | else ((scale - inactiveScale) / scalingInterval).coerceIn(0f, 1f) 525 | 526 | // Interpolate color between start and end color based on color scale 527 | val color = lerp(inactiveColor, activeColor, colorScale) 528 | 529 | val distanceToSelector = if (isRangeOfSelectionOdd) { 530 | itemCenter - selectorPosition 531 | } else { 532 | (itemCenter + itemSize / 2) - selectorPosition 533 | } 534 | 535 | return AnimationProgress( 536 | scale = scale, 537 | color = color, 538 | itemOffset = itemCenter, 539 | itemFraction = itemCenter / availableSpace, 540 | globalItemIndex = globalSelectedIndex, 541 | itemIndex = itemIndex, 542 | distanceToSelector = distanceToSelector 543 | ) 544 | } 545 | 546 | /** 547 | * Get index of item that is closest to selector position 548 | */ 549 | private fun getIndexClosestToSelector( 550 | selectedIndex: Int, 551 | halfItemSize: Float, 552 | selectorPosition: Float, 553 | visibleItems: List 554 | ): Int { 555 | var distance = Int.MAX_VALUE 556 | 557 | var globalSelectedIndex = selectedIndex 558 | 559 | visibleItems.forEach { 560 | val itemDistanceToSelector = ((it.offset + halfItemSize) - selectorPosition).absoluteValue 561 | if (itemDistanceToSelector < distance) { 562 | distance = itemDistanceToSelector.toInt() 563 | globalSelectedIndex = it.index 564 | } 565 | } 566 | 567 | return globalSelectedIndex 568 | } 569 | 570 | /** 571 | * Get position of selector item 572 | * ``` 573 | * 3 item range of selection <> = item, -- space between items 574 | * SELECTOR 575 | * --<>--<|>--<> 576 | * 4 item range of selection 577 | * SELECTOR 578 | * --<>--<>-|-<>--<>-- 579 | * ``` 580 | */ 581 | private fun getSelectorPosition( 582 | isRangeOfSelectionOdd: Boolean, 583 | indexOfSelector: Int, 584 | itemSize: Float, 585 | spaceBetweenItems: Float 586 | ): Float { 587 | val halfItemSize = itemSize / 2 588 | 589 | return if (isRangeOfSelectionOdd) { 590 | // Scale range is odd so selector position is the center of item 591 | // with indexOfSelector index 592 | (indexOfSelector) * (itemSize + spaceBetweenItems) + halfItemSize 593 | } else { 594 | // Scale range is even so selector position is space between indexOfSelector one 595 | // and next one 596 | (indexOfSelector) * (itemSize + spaceBetweenItems) + itemSize + spaceBetweenItems / 2 597 | } 598 | } 599 | 600 | /** 601 | * get scale based on whether it's initial run of list, 602 | * [LazyListState]'S [LazyListLayoutInfo.visibleItemsInfo] list is empty, 603 | * or current scroll state of list. 604 | * 605 | * @param inactiveScale lower scale that items can be scaled to. It should be less than 1f 606 | * @param itemSize width/height of each item 607 | * @param spaceBetweenItems space between each item 608 | * @param selectorPosition position of selector or selected item 609 | * @param itemCenter offset of item from start of the list's x or y zero position 610 | */ 611 | private fun getScale( 612 | rangeOfSelection: Int, 613 | selectorPosition: Float, 614 | itemCenter: Int, 615 | inactiveScale: Float, 616 | itemSize: Float, 617 | spaceBetweenItems: Float, 618 | ): Float { 619 | 620 | // Check how far this item is to selector index. 621 | val distanceToSelector = (selectorPosition - itemCenter).absoluteValue 622 | // When offset of an item is in this region it gets scaled. 623 | // region size is calculated as 624 | // half space + (item width or height) + half space 625 | val scaleRegionSize = (itemSize + spaceBetweenItems) * (rangeOfSelection + 1) / 2 626 | 627 | return calculateScale( 628 | distanceToSelector, 629 | scaleRegionSize, 630 | inactiveScale 631 | ) 632 | } 633 | 634 | /** 635 | * Calculate scale that is inside scale region based on [minimum]..1f range 636 | */ 637 | private fun calculateScale( 638 | distanceToSelector: Float, 639 | scaleRegionSize: Float, 640 | minimum: Float 641 | ): Float { 642 | return if (distanceToSelector < scaleRegionSize) { 643 | 644 | // item is in scale region. Check where exactly it is in this region 645 | val fraction = (scaleRegionSize - distanceToSelector) / scaleRegionSize 646 | 647 | // scale fraction between start and 1f. 648 | // If start is .9f and fraction is 50% our scale is .9f + .1f*50/100 = .95f 649 | lerp(start = minimum, stop = 1f, fraction = fraction) 650 | } else { 651 | minimum 652 | 653 | }.coerceIn(minimum, 1f) 654 | } 655 | -------------------------------------------------------------------------------- /animatedlist/src/main/java/com/smarttoolfactory/animatedlist/Color.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.animatedlist 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val ActiveColor = Color(0xffEC407A) 6 | val InactiveColor = Color.LightGray -------------------------------------------------------------------------------- /animatedlist/src/main/java/com/smarttoolfactory/animatedlist/ListSizeSubcomposeLayout.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.animatedlist 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.geometry.Size 6 | import androidx.compose.ui.layout.Measurable 7 | import androidx.compose.ui.layout.Placeable 8 | import androidx.compose.ui.layout.SubcomposeLayout 9 | import androidx.compose.ui.layout.SubcomposeMeasureScope 10 | import androidx.compose.ui.unit.Constraints 11 | 12 | /** 13 | * SubcomposeLayout that [SubcomposeMeasureScope.subcompose]s [mainContent] 14 | * and gets total size of [mainContent] and passes this size to [dependentContent]. 15 | * This layout passes exact size of content unlike 16 | * BoxWithConstraints which returns [Constraints] that doesn't match Composable dimensions under 17 | * some circumstances 18 | * 19 | * @param mainContent Composable is used for calculating size and pass it 20 | * to Composables that depend on it 21 | * 22 | * @param dependentContent Composable requires dimensions of [mainContent] to set its size. 23 | * One example for this is overlay over Composable that should match [mainContent] size. 24 | * 25 | */ 26 | @Composable 27 | internal fun ListSizeSubcomposeLayout( 28 | modifier: Modifier = Modifier, 29 | mainContent: @Composable () -> Unit, 30 | dependentContent: @Composable (Size) -> Unit 31 | ) { 32 | SubcomposeLayout( 33 | modifier = modifier 34 | ) { constraints: Constraints -> 35 | 36 | // Subcompose(compose only a section) main content and get Placeable 37 | val mainPlaceable: Placeable = subcompose(SlotsEnum.Main, mainContent) 38 | .map { 39 | it.measure(constraints.copy(minWidth = 0, minHeight = 0)) 40 | }.first() 41 | 42 | 43 | val dependentPlaceable: Placeable = 44 | subcompose(SlotsEnum.Dependent) { 45 | dependentContent( 46 | Size( 47 | mainPlaceable.width.toFloat(), 48 | mainPlaceable.height.toFloat() 49 | ) 50 | ) 51 | } 52 | .map { measurable: Measurable -> 53 | measurable.measure(constraints) 54 | }.first() 55 | 56 | 57 | layout(dependentPlaceable.width, dependentPlaceable.height) { 58 | dependentPlaceable.placeRelative(0, 0) 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * Enum class for SubcomposeLayouts with main and dependent Composables 65 | */ 66 | enum class SlotsEnum { Main, Dependent } 67 | -------------------------------------------------------------------------------- /animatedlist/src/main/java/com/smarttoolfactory/animatedlist/Util.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.animatedlist 2 | 3 | /** 4 | * [Linear Interpolation](https://en.wikipedia.org/wiki/Linear_interpolation) function that moves 5 | * amount from it's current position to start and amount 6 | * @param start of interval 7 | * @param stop of interval 8 | * @param fraction closed unit interval [0f, 1f] 9 | */ 10 | internal fun lerp(start: Float, stop: Float, fraction: Float): Float { 11 | return (1 - fraction) * start + fraction * stop 12 | } 13 | -------------------------------------------------------------------------------- /animatedlist/src/main/java/com/smarttoolfactory/animatedlist/model/ListAnimationModel.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.animatedlist.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | 6 | /** 7 | * Class that contains animated list animation properties 8 | * @param scale scale of the current item 9 | * @param color of the current item 10 | * @param itemOffset offset in px of current item from start of list 11 | * @param itemFraction offset in percent of current item from start of list relative to 12 | * width or height minus padding values in this axis 13 | * @param globalItemIndex index of current item. If it's returned from infinite list 14 | * it's real value of this index 15 | * @param itemIndex index of current item in range of total item count. It can be in range 16 | * of 0..item count -1 17 | * @param distanceToSelector distance of this item to selector in pixels. This value 18 | * can be used in a click listener to scroll this item to selection position with animation. 19 | */ 20 | @Immutable 21 | data class AnimationProgress( 22 | val scale: Float, 23 | val color: Color, 24 | val itemOffset: Int, 25 | val itemFraction: Float, 26 | val globalItemIndex: Int, 27 | val itemIndex: Int, 28 | val distanceToSelector: Float 29 | ) -------------------------------------------------------------------------------- /animatedlist/src/test/java/com/smarttoolfactory/animatedlist/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.animatedlist 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.smarttoolfactory.composeanimatedlist' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | minSdk 21 12 | targetSdk 33 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 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_17 26 | targetCompatibility JavaVersion.VERSION_17 27 | } 28 | kotlinOptions { 29 | jvmTarget = '17' 30 | } 31 | buildFeatures { 32 | compose true 33 | } 34 | composeOptions { 35 | kotlinCompilerExtensionVersion = "1.4.7" 36 | } 37 | 38 | packagingOptions { 39 | resources { 40 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | 47 | implementation project(':animatedlist') 48 | 49 | implementation 'androidx.core:core-ktx:1.10.1' 50 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 51 | 52 | 53 | implementation "dev.chrisbanes.snapper:snapper:0.3.0" 54 | 55 | implementation "androidx.compose.ui:ui:$compose_version" 56 | // Tooling support (Previews, etc.) 57 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 58 | // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) 59 | implementation "androidx.compose.foundation:foundation:$compose_version" 60 | // Material Design 61 | implementation "androidx.compose.material:material:$compose_version" 62 | // Material design icons 63 | implementation "androidx.compose.material:material-icons-core:$compose_version" 64 | implementation "androidx.compose.material:material-icons-extended:$compose_version" 65 | // Integration with activities 66 | implementation 'androidx.activity:activity-compose:1.7.2' 67 | // Integration with ViewModels 68 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1' 69 | 70 | // Material Design 3 for Compose 71 | implementation "androidx.compose.material3:material3:1.2.0-alpha02" 72 | 73 | def nav_compose_version = "2.5.3" 74 | implementation "androidx.navigation:navigation-compose:$nav_compose_version" 75 | 76 | def accompanist_version = "0.30.0" 77 | // Accompanist 78 | implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version" 79 | implementation "com.google.accompanist:accompanist-pager:$accompanist_version" 80 | implementation "com.google.accompanist:accompanist-drawablepainter:$accompanist_version" 81 | 82 | // Coil 83 | implementation("io.coil-kt:coil-compose:2.3.0") 84 | 85 | // Photo Picker 86 | implementation("com.google.modernstorage:modernstorage-photopicker:1.0.0-alpha06") 87 | 88 | testImplementation 'junit:junit:4.13.2' 89 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 90 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 91 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 92 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 93 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" 94 | } -------------------------------------------------------------------------------- /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/src/androidTest/java/com/smarttoolfactory/composeanimatedlist/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.smarttoolfactory.composeanimatedlist", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/AspectRatioModel.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | /** 6 | * Value class for containing aspect ratio 7 | * and [AspectRatio.Unspecified] for comparing 8 | */ 9 | @Immutable 10 | data class AspectRatio(val value: Float) { 11 | companion object { 12 | val Unspecified = AspectRatio(-1f) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/MainActivity.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalPagerApi::class) 2 | 3 | package com.smarttoolfactory.composeanimatedlist 4 | 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import com.google.accompanist.pager.ExperimentalPagerApi 17 | import com.smarttoolfactory.composeanimatedlist.demo.AnimatedInfiniteListDemo 18 | import com.smarttoolfactory.composeanimatedlist.demo.AnimatedInfiniteListDemo2 19 | import com.smarttoolfactory.composeanimatedlist.ui.theme.ComposeAnimatedListTheme 20 | 21 | class MainActivity : ComponentActivity() { 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContent { 25 | ComposeAnimatedListTheme { 26 | // A surface container using the 'background' color from the theme 27 | Surface( 28 | modifier = Modifier.fillMaxSize(), 29 | color = MaterialTheme.colorScheme.background 30 | ) { 31 | Column( 32 | modifier = Modifier 33 | .fillMaxSize() 34 | .background(Color.DarkGray) 35 | ) { 36 | PagerContent( 37 | content = mapOf Unit>( 38 | "InfiniteList" to { AnimatedInfiniteListDemo() }, 39 | "InfiniteList Params" to { AnimatedInfiniteListDemo2() } 40 | ) 41 | ) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/PagerContent.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.* 6 | import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.rememberCoroutineScope 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import com.google.accompanist.pager.ExperimentalPagerApi 12 | import com.google.accompanist.pager.HorizontalPager 13 | import com.google.accompanist.pager.PagerState 14 | import com.google.accompanist.pager.rememberPagerState 15 | import kotlinx.coroutines.launch 16 | 17 | @OptIn(ExperimentalMaterial3Api::class) 18 | @ExperimentalPagerApi 19 | @Composable 20 | fun PagerContent(content: Map Unit>) { 21 | 22 | val pagerState: PagerState = rememberPagerState(initialPage = 0) 23 | 24 | val coroutineScope = rememberCoroutineScope() 25 | 26 | val tabList = content.keys.toList() 27 | val pages: List<@Composable () -> Unit> = content.values.toList() 28 | 29 | Scaffold( 30 | topBar = { 31 | if (content.size < 3) { 32 | TabRow( 33 | modifier = Modifier.fillMaxWidth(), 34 | // Our selected tab is our current page 35 | selectedTabIndex = pagerState.currentPage, 36 | // Override the indicator, using the provided pagerTabIndicatorOffset modifier 37 | indicator = { tabPositions: List -> 38 | TabRowDefaults.Indicator( 39 | modifier = Modifier.tabIndicatorOffset( 40 | tabPositions[pagerState.currentPage] 41 | ), 42 | height = 4.dp 43 | ) 44 | }, 45 | ) { 46 | // Add tabs for all of our pages 47 | tabList.forEachIndexed { index, title -> 48 | Tab( 49 | text = { Text(title) }, 50 | selected = pagerState.currentPage == index, 51 | onClick = { 52 | coroutineScope.launch { 53 | pagerState.animateScrollToPage(index) 54 | } 55 | } 56 | ) 57 | } 58 | } 59 | } else { 60 | ScrollableTabRow( 61 | modifier = Modifier.fillMaxWidth(), 62 | // Our selected tab is our current page 63 | selectedTabIndex = pagerState.currentPage, 64 | // Override the indicator, using the provided pagerTabIndicatorOffset modifier 65 | indicator = { tabPositions: List -> 66 | TabRowDefaults.Indicator( 67 | modifier = Modifier.tabIndicatorOffset( 68 | tabPositions[pagerState.currentPage] 69 | ), 70 | height = 4.dp 71 | ) 72 | }, 73 | edgePadding = 4.dp 74 | ) { 75 | // Add tabs for all of our pages 76 | tabList.forEachIndexed { index, title -> 77 | Tab( 78 | text = { Text(title) }, 79 | selected = pagerState.currentPage == index, 80 | onClick = { 81 | coroutineScope.launch { 82 | pagerState.animateScrollToPage(index) 83 | } 84 | } 85 | ) 86 | } 87 | } 88 | } 89 | } 90 | ) { 91 | HorizontalPager( 92 | modifier = Modifier.padding(it), 93 | state = pagerState, 94 | count = content.size 95 | ) { page: Int -> 96 | pages[page].invoke() 97 | 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/ShapeModel.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Shape 5 | 6 | /** 7 | * Model for drawing title with shape for crop selection menu. Aspect ratio is used 8 | * for setting overlay in state and UI 9 | */ 10 | @Immutable 11 | data class ShapeModel( 12 | val title: String, 13 | val shape: Shape, 14 | val aspectRatio: AspectRatio = AspectRatio.Unspecified 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/ShapeSelection.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.draw.drawWithContent 11 | import androidx.compose.ui.draw.shadow 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.drawOutline 14 | import androidx.compose.ui.graphics.drawscope.Stroke 15 | import androidx.compose.ui.graphics.drawscope.translate 16 | import androidx.compose.ui.platform.LocalDensity 17 | import androidx.compose.ui.platform.LocalLayoutDirection 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | 21 | 22 | @Composable 23 | fun ShapeSelection( 24 | modifier: Modifier = Modifier, 25 | color: Color, 26 | shapeModel: ShapeModel 27 | ) { 28 | Box( 29 | modifier = modifier 30 | .shadow(1.dp, RoundedCornerShape(8.dp)) 31 | .background(Color.White) 32 | .padding(4.dp) 33 | ) { 34 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 35 | val density = LocalDensity.current 36 | val layoutDirection = LocalLayoutDirection.current 37 | Box(modifier = Modifier 38 | .fillMaxWidth() 39 | .padding(4.dp) 40 | .aspectRatio(1f) 41 | .drawWithContent { 42 | 43 | val outline = shapeModel.shape.createOutline( 44 | size = size, 45 | layoutDirection = layoutDirection, 46 | density = density 47 | ) 48 | 49 | val width = size.width 50 | val height = size.height 51 | val outlineWidth = outline.bounds.width 52 | val outlineHeight = outline.bounds.height 53 | 54 | translate( 55 | left = (width - outlineWidth) / 2, 56 | top = (height - outlineHeight) / 2 57 | ) { 58 | drawOutline( 59 | outline = outline, 60 | color = color, 61 | style = Stroke(3.dp.toPx()) 62 | ) 63 | } 64 | } 65 | ) 66 | Text(text = shapeModel.title, color = color, fontSize = 14.sp) 67 | } 68 | } 69 | } 70 | 71 | @Composable 72 | fun ShapeSelection( 73 | modifier: Modifier = Modifier, 74 | isSelected: Boolean, 75 | shapeModel: ShapeModel 76 | ) { 77 | Box( 78 | modifier = modifier 79 | .shadow(1.dp, RoundedCornerShape(8.dp)) 80 | .background(Color.White) 81 | .padding(4.dp) 82 | ) { 83 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 84 | val density = LocalDensity.current 85 | val layoutDirection = LocalLayoutDirection.current 86 | val color = if (isSelected) Color.Cyan else Color.LightGray 87 | Box(modifier = Modifier 88 | .fillMaxWidth() 89 | .padding(4.dp) 90 | .aspectRatio(1f) 91 | .drawWithContent { 92 | 93 | val outline = shapeModel.shape.createOutline( 94 | size = size, 95 | layoutDirection = layoutDirection, 96 | density = density 97 | ) 98 | 99 | val width = size.width 100 | val height = size.height 101 | val outlineWidth = outline.bounds.width 102 | val outlineHeight = outline.bounds.height 103 | 104 | translate( 105 | left = (width - outlineWidth) / 2, 106 | top = (height - outlineHeight) / 2 107 | ) { 108 | drawOutline( 109 | outline = outline, 110 | color = color, 111 | style = Stroke(3.dp.toPx()) 112 | ) 113 | } 114 | } 115 | ) 116 | Text(text = shapeModel.title, color = color, fontSize = 14.sp) 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/Shapes.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist 2 | 3 | import androidx.compose.foundation.shape.GenericShape 4 | import androidx.compose.ui.geometry.Offset 5 | import androidx.compose.ui.geometry.Rect 6 | import androidx.compose.ui.geometry.Size 7 | import androidx.compose.ui.unit.LayoutDirection 8 | 9 | /** 10 | * Aspect ratio list with pre-defined aspect ratios 11 | */ 12 | val aspectRatios = listOf( 13 | ShapeModel("9:16", createRectShape(9 / 16f)), 14 | ShapeModel("2:3", createRectShape(2 / 3f)), 15 | ShapeModel("1:1", createRectShape(1 / 1f)), 16 | ShapeModel("16:9", createRectShape(16 / 9f)), 17 | ShapeModel("1.91:1", createRectShape(1.91f / 1f)), 18 | ShapeModel("3:2", createRectShape(3 / 2f)), 19 | ShapeModel("3:4", createRectShape(3 / 4f)), 20 | ShapeModel("3:5", createRectShape(3 / 5f)), 21 | ) 22 | 23 | /** 24 | * Creates a [Rect] shape with given aspect ratio. 25 | */ 26 | fun createRectShape(aspectRatio: Float): GenericShape { 27 | return GenericShape { size: Size, _: LayoutDirection -> 28 | 29 | val width = size.width 30 | val height = size.height 31 | val shapeSize = if (aspectRatio > 1) Size(width = width, height = width / aspectRatio) 32 | else Size(width = height * aspectRatio, height = height) 33 | 34 | addRect(Rect(offset = Offset.Zero, size = shapeSize)) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/Snack.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.ElevatedCard 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.Immutable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.layout.ContentScale 10 | import androidx.compose.ui.platform.LocalContext 11 | import coil.compose.rememberAsyncImagePainter 12 | import coil.request.ImageRequest 13 | import coil.size.Size 14 | 15 | @Immutable 16 | data class Snack( 17 | val id: Long, 18 | val name: String, 19 | val imageUrl: String, 20 | val price: Long, 21 | val tagline: String = "", 22 | val tags: Set = emptySet() 23 | ) 24 | 25 | @Composable 26 | fun SnackCard( 27 | modifier: Modifier, 28 | snack: Snack) { 29 | val painter = rememberAsyncImagePainter( 30 | model = ImageRequest.Builder(LocalContext.current) 31 | .data(snack.imageUrl) 32 | .placeholder(R.drawable.placeholder) 33 | .size(Size.ORIGINAL) // Set the target size to load the image at. 34 | .build() 35 | ) 36 | 37 | 38 | ElevatedCard( 39 | modifier = modifier 40 | ) { 41 | Image( 42 | modifier=Modifier.fillMaxSize(), 43 | contentScale = ContentScale.FillBounds, 44 | painter = painter, 45 | contentDescription = null 46 | ) 47 | } 48 | 49 | } 50 | 51 | val snacks = listOf( 52 | Snack( 53 | id = 1L, 54 | name = "Cupcake", 55 | tagline = "A tag line", 56 | imageUrl = "https://source.unsplash.com/pGM4sjt_BdQ", 57 | price = 299 58 | ), 59 | Snack( 60 | id = 2L, 61 | name = "Donut", 62 | tagline = "A tag line", 63 | imageUrl = "https://source.unsplash.com/Yc5sL-ejk6U", 64 | price = 299 65 | ), 66 | Snack( 67 | id = 3L, 68 | name = "Eclair", 69 | tagline = "A tag line", 70 | imageUrl = "https://source.unsplash.com/-LojFX9NfPY", 71 | price = 299 72 | ), 73 | Snack( 74 | id = 4L, 75 | name = "Froyo", 76 | tagline = "A tag line", 77 | imageUrl = "https://source.unsplash.com/3U2V5WqK1PQ", 78 | price = 299 79 | ), 80 | Snack( 81 | id = 5L, 82 | name = "Gingerbread", 83 | tagline = "A tag line", 84 | imageUrl = "https://source.unsplash.com/Y4YR9OjdIMk", 85 | price = 499 86 | ), 87 | Snack( 88 | id = 6L, 89 | name = "Honeycomb", 90 | tagline = "A tag line", 91 | imageUrl = "https://source.unsplash.com/bELvIg_KZGU", 92 | price = 299 93 | ), 94 | Snack( 95 | id = 7L, 96 | name = "Ice Cream Sandwich", 97 | tagline = "A tag line", 98 | imageUrl = "https://source.unsplash.com/YgYJsFDd4AU", 99 | price = 1299 100 | ), 101 | Snack( 102 | id = 8L, 103 | name = "Jellybean", 104 | tagline = "A tag line", 105 | imageUrl = "https://source.unsplash.com/0u_vbeOkMpk", 106 | price = 299 107 | ), 108 | Snack( 109 | id = 9L, 110 | name = "KitKat", 111 | tagline = "A tag line", 112 | imageUrl = "https://source.unsplash.com/yb16pT5F_jE", 113 | price = 549 114 | ), 115 | Snack( 116 | id = 10L, 117 | name = "Lollipop", 118 | tagline = "A tag line", 119 | imageUrl = "https://source.unsplash.com/AHF_ZktTL6Q", 120 | price = 299 121 | ), 122 | Snack( 123 | id = 11L, 124 | name = "Marshmallow", 125 | tagline = "A tag line", 126 | imageUrl = "https://source.unsplash.com/rqFm0IgMVYY", 127 | price = 299 128 | ), 129 | Snack( 130 | id = 12L, 131 | name = "Nougat", 132 | tagline = "A tag line", 133 | imageUrl = "https://source.unsplash.com/qRE_OpbVPR8", 134 | price = 299 135 | ), 136 | Snack( 137 | id = 13L, 138 | name = "Oreo", 139 | tagline = "A tag line", 140 | imageUrl = "https://source.unsplash.com/33fWPnyN6tU", 141 | price = 299 142 | ), 143 | Snack( 144 | id = 14L, 145 | name = "Pie", 146 | tagline = "A tag line", 147 | imageUrl = "https://source.unsplash.com/aX_ljOOyWJY", 148 | price = 299 149 | ), 150 | Snack( 151 | id = 15L, 152 | name = "Chips", 153 | imageUrl = "https://source.unsplash.com/UsSdMZ78Q3E", 154 | price = 299 155 | ), 156 | Snack( 157 | id = 16L, 158 | name = "Pretzels", 159 | imageUrl = "https://source.unsplash.com/7meCnGCJ5Ms", 160 | price = 299 161 | ), 162 | Snack( 163 | id = 17L, 164 | name = "Smoothies", 165 | imageUrl = "https://source.unsplash.com/m741tj4Cz7M", 166 | price = 299 167 | ), 168 | Snack( 169 | id = 18L, 170 | name = "Popcorn", 171 | imageUrl = "https://source.unsplash.com/iuwMdNq0-s4", 172 | price = 299 173 | ), 174 | Snack( 175 | id = 19L, 176 | name = "Almonds", 177 | imageUrl = "https://source.unsplash.com/qgWWQU1SzqM", 178 | price = 299 179 | ), 180 | Snack( 181 | id = 20L, 182 | name = "Cheese", 183 | imageUrl = "https://source.unsplash.com/9MzCd76xLGk", 184 | price = 299 185 | ), 186 | Snack( 187 | id = 21L, 188 | name = "Apples", 189 | tagline = "A tag line", 190 | imageUrl = "https://source.unsplash.com/1d9xXWMtQzQ", 191 | price = 299 192 | ), 193 | Snack( 194 | id = 22L, 195 | name = "Apple sauce", 196 | tagline = "A tag line", 197 | imageUrl = "https://source.unsplash.com/wZxpOw84QTU", 198 | price = 299 199 | ), 200 | Snack( 201 | id = 23L, 202 | name = "Apple chips", 203 | tagline = "A tag line", 204 | imageUrl = "https://source.unsplash.com/okzeRxm_GPo", 205 | price = 299 206 | ), 207 | Snack( 208 | id = 24L, 209 | name = "Apple juice", 210 | tagline = "A tag line", 211 | imageUrl = "https://source.unsplash.com/l7imGdupuhU", 212 | price = 299 213 | ), 214 | Snack( 215 | id = 25L, 216 | name = "Apple pie", 217 | tagline = "A tag line", 218 | imageUrl = "https://source.unsplash.com/bkXzABDt08Q", 219 | price = 299 220 | ), 221 | Snack( 222 | id = 26L, 223 | name = "Grapes", 224 | tagline = "A tag line", 225 | imageUrl = "https://source.unsplash.com/y2MeW00BdBo", 226 | price = 299 227 | ), 228 | Snack( 229 | id = 27L, 230 | name = "Kiwi", 231 | tagline = "A tag line", 232 | imageUrl = "https://source.unsplash.com/1oMGgHn-M8k", 233 | price = 299 234 | ), 235 | Snack( 236 | id = 28L, 237 | name = "Mango", 238 | tagline = "A tag line", 239 | imageUrl = "https://source.unsplash.com/TIGDsyy0TK4", 240 | price = 299 241 | ) 242 | ) 243 | -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/demo/AnimatedInfiniteListDemo.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist.demo 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.gestures.animateScrollBy 6 | import androidx.compose.foundation.interaction.MutableInteractionSource 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.layout.width 16 | import androidx.compose.foundation.shape.CircleShape 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableIntStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.rememberCoroutineScope 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.draw.scale 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.graphics.graphicsLayer 29 | import androidx.compose.ui.platform.LocalDensity 30 | import androidx.compose.ui.text.font.FontWeight 31 | import androidx.compose.ui.tooling.preview.Preview 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.unit.sp 34 | import com.smarttoolfactory.animatedlist.ActiveColor 35 | import com.smarttoolfactory.animatedlist.AnimatedInfiniteLazyColumn 36 | import com.smarttoolfactory.animatedlist.AnimatedInfiniteLazyRow 37 | import com.smarttoolfactory.animatedlist.InactiveColor 38 | import com.smarttoolfactory.composeanimatedlist.ShapeSelection 39 | import com.smarttoolfactory.composeanimatedlist.SnackCard 40 | import com.smarttoolfactory.composeanimatedlist.aspectRatios 41 | import com.smarttoolfactory.composeanimatedlist.snacks 42 | import kotlinx.coroutines.launch 43 | 44 | @Preview 45 | @Composable 46 | fun AnimatedInfiniteListDemo() { 47 | Column( 48 | modifier = Modifier 49 | .fillMaxSize() 50 | .background(Color.DarkGray), 51 | horizontalAlignment = Alignment.CenterHorizontally 52 | ) { 53 | 54 | val coroutineScope = rememberCoroutineScope() 55 | 56 | Spacer(modifier = Modifier.height(40.dp)) 57 | 58 | val listWidth = LocalDensity.current.run { 1000.toDp() } 59 | val spaceBetweenItems = LocalDensity.current.run { 30.toDp() } 60 | 61 | // Demonstrating for setting first visible item of list if we want to 62 | // make last second item from the end and last item as initial selected item 63 | val initialVisibleItem = 0 64 | val visibleItemCount = 5 65 | val initialSelectedItem = 2 66 | 67 | var selectedItem by remember { 68 | mutableIntStateOf(initialSelectedItem) 69 | } 70 | 71 | AnimatedInfiniteLazyRow( 72 | modifier = Modifier.width(listWidth), 73 | items = aspectRatios, 74 | visibleItemCount = visibleItemCount, 75 | initialFirstVisibleIndex = initialVisibleItem, 76 | inactiveColor = InactiveColor, 77 | activeColor = ActiveColor, 78 | itemContent = { animationProgress, index, item, width, lazyListState -> 79 | 80 | val scale = animationProgress.scale 81 | val color = animationProgress.color 82 | 83 | selectedItem = animationProgress.itemIndex 84 | 85 | ShapeSelection(modifier = Modifier 86 | .graphicsLayer { 87 | scaleY = scale 88 | alpha = scale 89 | } 90 | .width(width) 91 | .clickable( 92 | interactionSource = remember { 93 | MutableInteractionSource() 94 | }, 95 | indication = null 96 | ) { 97 | coroutineScope.launch { 98 | lazyListState.animateScrollBy(animationProgress.distanceToSelector) 99 | } 100 | }, 101 | color = color, 102 | shapeModel = item 103 | ) 104 | } 105 | ) 106 | 107 | Text( 108 | text = "Selected item ${aspectRatios[selectedItem].title}", 109 | fontSize = 20.sp, 110 | color = ActiveColor 111 | ) 112 | 113 | Spacer(modifier = Modifier.height(20.dp)) 114 | 115 | AnimatedInfiniteLazyRow( 116 | modifier = Modifier 117 | .width(listWidth) 118 | .padding(10.dp), 119 | items = aspectRatios, 120 | visibleItemCount = 5, 121 | activeItemWidth = 40.dp, 122 | inactiveItemWidth = 30.dp, 123 | selectorIndex = 1, 124 | spaceBetweenItems = 0.dp, 125 | inactiveColor = InactiveColor, 126 | activeColor = ActiveColor, 127 | itemContent = { animationProgress, index, item, width, lazyListState -> 128 | val color = animationProgress.color 129 | val scale = animationProgress.scale 130 | Box( 131 | modifier = Modifier 132 | .scale(scale) 133 | .background(color, CircleShape) 134 | .size(width) 135 | .clickable( 136 | interactionSource = remember { 137 | MutableInteractionSource() 138 | }, 139 | indication = null 140 | ) { 141 | coroutineScope.launch { 142 | lazyListState.animateScrollBy(animationProgress.distanceToSelector) 143 | } 144 | }, 145 | contentAlignment = Alignment.Center 146 | ) { 147 | 148 | Text( 149 | "$index", 150 | color = Color.White, 151 | fontSize = 20.sp, 152 | fontWeight = FontWeight.Bold 153 | ) 154 | } 155 | } 156 | ) 157 | Spacer(modifier = Modifier.height(20.dp)) 158 | 159 | AnimatedInfiniteLazyRow( 160 | modifier = Modifier.width(300.dp), 161 | items = aspectRatios, 162 | visibleItemCount = 7, 163 | selectorIndex = 3, 164 | inactiveColor = InactiveColor, 165 | activeColor = ActiveColor, 166 | inactiveItemPercent = 70, 167 | itemContent = { animationProgress, index, item, size, lazyListState -> 168 | 169 | val color = animationProgress.color 170 | val scale = animationProgress.scale 171 | 172 | Box( 173 | modifier = Modifier 174 | .scale(scale) 175 | .background(color, CircleShape) 176 | .size(size) 177 | .clickable( 178 | interactionSource = remember { 179 | MutableInteractionSource() 180 | }, 181 | indication = null 182 | ) { 183 | coroutineScope.launch { 184 | lazyListState.animateScrollBy(animationProgress.distanceToSelector) 185 | } 186 | }, 187 | contentAlignment = Alignment.Center 188 | ) { 189 | 190 | Text( 191 | "$index", 192 | color = Color.White, 193 | fontSize = 20.sp, 194 | fontWeight = FontWeight.Bold 195 | ) 196 | } 197 | } 198 | ) 199 | 200 | Spacer(modifier = Modifier.height(20.dp)) 201 | 202 | AnimatedInfiniteLazyRow( 203 | modifier = Modifier 204 | .width(listWidth) 205 | .height(150.dp), 206 | items = snacks, 207 | visibleItemCount = 3, 208 | spaceBetweenItems = spaceBetweenItems, 209 | inactiveItemPercent = 70, 210 | inactiveColor = InactiveColor, 211 | activeColor = ActiveColor 212 | ) { animationProgress, index, item, size, lazyListState -> 213 | 214 | val scale = animationProgress.scale 215 | 216 | SnackCard( 217 | modifier = Modifier 218 | .graphicsLayer { 219 | scaleX = scale 220 | scaleY = scale 221 | alpha = scale 222 | } 223 | .fillMaxWidth() 224 | .width(size) 225 | .clickable( 226 | interactionSource = remember { 227 | MutableInteractionSource() 228 | }, 229 | indication = null 230 | ) { 231 | coroutineScope.launch { 232 | lazyListState.animateScrollBy(animationProgress.distanceToSelector) 233 | } 234 | }, 235 | snack = item 236 | ) 237 | 238 | } 239 | 240 | Spacer(modifier = Modifier.height(20.dp)) 241 | 242 | AnimatedInfiniteLazyColumn( 243 | items = aspectRatios, 244 | visibleItemCount = 5, 245 | inactiveColor = InactiveColor, 246 | activeColor = ActiveColor, 247 | selectorIndex = 0, 248 | inactiveItemPercent = 70, 249 | itemContent = { animationProgress, index, item, height, lazyListState -> 250 | 251 | val color = animationProgress.color 252 | val scale = animationProgress.scale 253 | 254 | Box( 255 | modifier = Modifier 256 | .scale(scale) 257 | .background(color, CircleShape) 258 | .size(height) 259 | .clickable( 260 | interactionSource = remember { 261 | MutableInteractionSource() 262 | }, 263 | indication = null 264 | ) { 265 | coroutineScope.launch { 266 | lazyListState.animateScrollBy(animationProgress.distanceToSelector) 267 | } 268 | }, 269 | contentAlignment = Alignment.Center 270 | ) { 271 | 272 | Text( 273 | "$index", 274 | color = Color.White, 275 | fontSize = 20.sp, 276 | fontWeight = FontWeight.Bold 277 | ) 278 | } 279 | } 280 | ) 281 | } 282 | } -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/demo/AnimatedInfiniteListDemo2.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist.demo 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.gestures.animateScrollBy 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.height 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.layout.size 17 | import androidx.compose.foundation.layout.width 18 | import androidx.compose.foundation.shape.CircleShape 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.Slider 21 | import androidx.compose.material3.Switch 22 | import androidx.compose.material3.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.mutableFloatStateOf 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.runtime.rememberCoroutineScope 29 | import androidx.compose.runtime.setValue 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.draw.clip 33 | import androidx.compose.ui.draw.scale 34 | import androidx.compose.ui.graphics.Color 35 | import androidx.compose.ui.graphics.graphicsLayer 36 | import androidx.compose.ui.text.font.FontWeight 37 | import androidx.compose.ui.tooling.preview.Preview 38 | import androidx.compose.ui.unit.dp 39 | import androidx.compose.ui.unit.sp 40 | import com.smarttoolfactory.animatedlist.ActiveColor 41 | import com.smarttoolfactory.animatedlist.AnimatedInfiniteLazyRow 42 | import com.smarttoolfactory.animatedlist.InactiveColor 43 | import com.smarttoolfactory.composeanimatedlist.SnackCard 44 | import com.smarttoolfactory.composeanimatedlist.aspectRatios 45 | import com.smarttoolfactory.composeanimatedlist.snacks 46 | import kotlinx.coroutines.launch 47 | 48 | @Preview 49 | @Composable 50 | fun AnimatedInfiniteListDemo2() { 51 | Column( 52 | modifier = Modifier 53 | .fillMaxSize() 54 | .background(Color.DarkGray) 55 | .padding(10.dp), 56 | horizontalAlignment = Alignment.CenterHorizontally 57 | ) { 58 | Spacer(modifier = Modifier.height(40.dp)) 59 | 60 | val coroutineScope = rememberCoroutineScope() 61 | 62 | var visibleIteCount by remember { mutableFloatStateOf(5f) } 63 | var selectorIndex by remember { mutableFloatStateOf(2f) } 64 | var itemScaleRange by remember { mutableFloatStateOf(1f) } 65 | var inactiveItemFraction by remember { mutableFloatStateOf(70f) } 66 | 67 | var showPartialItem by remember { mutableStateOf(false) } 68 | 69 | 70 | Text( 71 | "Visible Item Count: ${visibleIteCount.toInt()}", 72 | color = MaterialTheme.colorScheme.inversePrimary, 73 | fontSize = 18.sp 74 | ) 75 | Slider( 76 | value = visibleIteCount, 77 | onValueChange = { 78 | visibleIteCount = it 79 | }, 80 | steps = 7, 81 | valueRange = 3f..11f 82 | ) 83 | 84 | Text( 85 | "Selector Index: ${selectorIndex.toInt()}", 86 | color = MaterialTheme.colorScheme.inversePrimary, 87 | fontSize = 18.sp 88 | ) 89 | Slider( 90 | value = selectorIndex, 91 | onValueChange = { 92 | selectorIndex = it 93 | }, 94 | steps = (visibleIteCount - 2).toInt(), 95 | valueRange = 0f..(visibleIteCount - 1) 96 | ) 97 | 98 | Text( 99 | "Scale Range: ${itemScaleRange.toInt()}", 100 | color = MaterialTheme.colorScheme.inversePrimary, 101 | fontSize = 18.sp 102 | ) 103 | Slider( 104 | value = itemScaleRange, 105 | onValueChange = { 106 | itemScaleRange = it 107 | }, 108 | steps = (visibleIteCount - 2).toInt(), 109 | valueRange = 1f..visibleIteCount 110 | ) 111 | 112 | Text( 113 | "Inactive Item Percent: ${inactiveItemFraction.toInt()}", 114 | color = MaterialTheme.colorScheme.inversePrimary, 115 | fontSize = 18.sp 116 | ) 117 | Slider( 118 | value = inactiveItemFraction, 119 | onValueChange = { 120 | inactiveItemFraction = it 121 | }, 122 | valueRange = 0f..100f 123 | ) 124 | 125 | Row(verticalAlignment = Alignment.CenterVertically) { 126 | Text( 127 | "Partial Items", 128 | color = MaterialTheme.colorScheme.inversePrimary, 129 | fontSize = 18.sp 130 | ) 131 | Spacer(modifier = Modifier.width(20.dp)) 132 | Switch(checked = showPartialItem, onCheckedChange = { showPartialItem = it }) 133 | } 134 | 135 | Spacer(modifier = Modifier.height(30.dp)) 136 | 137 | AnimatedInfiniteLazyRow( 138 | modifier = Modifier 139 | .fillMaxWidth() 140 | .height(100.dp), 141 | items = snacks, 142 | visibleItemCount = visibleIteCount.toInt(), 143 | selectorIndex = selectorIndex.toInt(), 144 | itemScaleRange = itemScaleRange.toInt(), 145 | showPartialItem = showPartialItem, 146 | spaceBetweenItems = 16.dp, 147 | inactiveItemPercent = inactiveItemFraction.toInt(), 148 | inactiveColor = InactiveColor, 149 | activeColor = ActiveColor 150 | ) { animationProgress, index, item, width, lazyListState -> 151 | 152 | val scale = animationProgress.scale 153 | 154 | SnackCard( 155 | modifier = Modifier 156 | .graphicsLayer { 157 | scaleX = scale 158 | scaleY = scale 159 | alpha = scale 160 | } 161 | .clip(CircleShape) 162 | .size(width) 163 | .clickable( 164 | interactionSource = remember { 165 | MutableInteractionSource() 166 | }, 167 | indication = null 168 | ) { 169 | coroutineScope.launch { 170 | lazyListState.animateScrollBy(animationProgress.distanceToSelector) 171 | } 172 | }, 173 | snack = item 174 | ) 175 | } 176 | 177 | Spacer(modifier = Modifier.height(30.dp)) 178 | 179 | AnimatedInfiniteLazyRow( 180 | modifier = Modifier 181 | .padding(20.dp) 182 | .fillMaxWidth() 183 | .border(1.dp, Color.Green), 184 | items = aspectRatios, 185 | visibleItemCount = visibleIteCount.toInt(), 186 | selectorIndex = selectorIndex.toInt(), 187 | itemScaleRange = itemScaleRange.toInt(), 188 | showPartialItem = showPartialItem, 189 | inactiveItemPercent = inactiveItemFraction.toInt(), 190 | inactiveColor = InactiveColor, 191 | activeColor = ActiveColor, 192 | itemContent = { animationProgress, index, item, width, lazyListState -> 193 | val color = animationProgress.color 194 | val scale = animationProgress.scale 195 | Box( 196 | modifier = Modifier 197 | .scale(scale) 198 | .background(color, CircleShape) 199 | .size(width) 200 | .clickable( 201 | interactionSource = remember { 202 | MutableInteractionSource() 203 | }, 204 | indication = null 205 | ) { 206 | coroutineScope.launch { 207 | lazyListState.animateScrollBy(animationProgress.distanceToSelector) 208 | } 209 | }, 210 | contentAlignment = Alignment.Center 211 | ) { 212 | 213 | Text( 214 | "$index", 215 | color = Color.White, 216 | fontSize = 20.sp, 217 | fontWeight = FontWeight.Bold 218 | ) 219 | } 220 | } 221 | ) 222 | } 223 | } -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.ViewCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun ComposeAnimatedListTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | darkTheme -> DarkColorScheme 53 | else -> LightColorScheme 54 | } 55 | val view = LocalView.current 56 | if (!view.isInEditMode) { 57 | SideEffect { 58 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 59 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 60 | } 61 | } 62 | 63 | MaterialTheme( 64 | colorScheme = colorScheme, 65 | typography = Typography, 66 | content = content 67 | ) 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/composeanimatedlist/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.composeanimatedlist.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/drawable/placeholder.jpg -------------------------------------------------------------------------------- /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.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Compose-AnimatedList/1f376a1c079cf8169b7d56950c0d102e6e7b9ded/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Compose AnimatedList 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |