├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── represa │ │ └── draw │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── represa │ │ │ └── draw │ │ │ ├── DotsIndicator.kt │ │ │ ├── MainActivity.kt │ │ │ ├── PhysicoChest.kt │ │ │ ├── data │ │ │ └── Dessert.kt │ │ │ ├── extensions │ │ │ ├── LazyRow.kt │ │ │ ├── Other.kt │ │ │ └── PagerState.kt │ │ │ └── ui │ │ │ ├── BottomBar.kt │ │ │ ├── DatePicker.kt │ │ │ ├── DessertCard.kt │ │ │ ├── Indicators.kt │ │ │ ├── RoundIndicators.kt │ │ │ ├── Splash.kt │ │ │ ├── Stars.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── 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-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── represa │ └── draw │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | /*/build/ 3 | 4 | # Crashlytics configuations 5 | com_crashlytics_export_strings.xml 6 | 7 | # Local configuration file (sdk path, etc) 8 | local.properties 9 | 10 | # Gradle generated files 11 | .gradle/ 12 | 13 | # Signing files 14 | .signing/ 15 | 16 | # User-specific configurations 17 | .idea/* 18 | *.iml 19 | 20 | # OS-specific files 21 | .DS_Store 22 | .DS_Store? 23 | ._* 24 | .Spotlight-V100 25 | .Trashes 26 | ehthumbs.db 27 | Thumbs.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compose Animations🚀 2 | 3 | In this repo you will find a series of animations made with Jetpack Compose 4 | 5 | ## Current Animations 6 | 7 | ⚡️ Bottom Bar 8 | 9 | ![ezgif com-video-to-gif-12](https://user-images.githubusercontent.com/17813148/127470732-22a237bd-2bd0-40a4-b4e9-1f4ab4b458f4.gif) 10 | 11 | 12 | :page_facing_up: Indicators 13 | 14 | ![ezgif com-video-to-gif-5](https://user-images.githubusercontent.com/17813148/120201649-f54d6280-c225-11eb-8522-dbe6726fa004.gif) 15 | 16 | ![ezgif com-video-to-gif-10](https://user-images.githubusercontent.com/17813148/125770160-c4bc0b94-d8ce-49d1-9d1f-9bd7cf485751.gif) 17 | 18 | 19 | 🗓Airbnb Animated Calendar 20 | 21 | ![ezgif com-video-to-gif-7](https://user-images.githubusercontent.com/17813148/121699513-4b948e00-cacf-11eb-94e1-0ab4c8e275f2.gif) 22 | 23 | ♟Chest 24 | 25 | ![ezgif com-optimize](https://user-images.githubusercontent.com/17813148/120201260-88d26380-c225-11eb-9bb1-a9595533b698.gif) 26 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | applicationId "com.represa.draw" 12 | minSdkVersion 21 13 | targetSdkVersion 30 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | useIR = true 33 | } 34 | buildFeatures { 35 | compose true 36 | } 37 | composeOptions { 38 | kotlinCompilerExtensionVersion compose_version 39 | kotlinCompilerVersion '1.5.10' 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 45 | implementation 'androidx.core:core-ktx:1.3.2' 46 | implementation 'androidx.appcompat:appcompat:1.2.0' 47 | implementation 'com.google.android.material:material:1.3.0' 48 | implementation "androidx.compose.ui:ui:$compose_version" 49 | implementation "androidx.compose.material:material:$compose_version" 50 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 51 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' 52 | implementation 'androidx.activity:activity-compose:1.3.0-alpha08' 53 | testImplementation 'junit:junit:4.+' 54 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 55 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 56 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 57 | // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) 58 | implementation "androidx.compose.foundation:foundation:$compose_version" 59 | // Material design icons 60 | implementation "androidx.compose.material:material-icons-core:$compose_version" 61 | implementation "androidx.compose.material:material-icons-extended:$compose_version" 62 | // Integration with ViewModels 63 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha05' 64 | // Integration with observables 65 | implementation "androidx.compose.runtime:runtime-livedata:$compose_version" 66 | // UI Tests 67 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.0.0-beta01' 68 | 69 | implementation "androidx.compose.ui:ui-tooling:1.0.0-beta01" 70 | 71 | //Accompanist 72 | implementation "com.google.accompanist:accompanist-systemuicontroller:0.15.0" 73 | implementation "com.google.accompanist:accompanist-pager:0.15.0" 74 | implementation("io.coil-kt:coil-compose:1.3.1") 75 | 76 | //Navigation 77 | implementation "androidx.navigation:navigation-compose:2.4.0-alpha05" 78 | } -------------------------------------------------------------------------------- /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/represa/draw/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw 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.represa.draw", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/DotsIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.geometry.Offset 9 | import androidx.compose.ui.graphics.Color 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 com.represa.draw.data.desserts 16 | import com.represa.draw.extensions.targetValue 17 | import com.represa.draw.ui.* 18 | 19 | @ExperimentalPagerApi 20 | @Composable 21 | fun DotsIndicator() { 22 | var scope = rememberCoroutineScope() 23 | val list = remember { desserts } 24 | val dotSettings = 25 | IndicatorState.DotSettings(size = list.size, radius = 12f, color = Color.Black) 26 | val pagerState = rememberPagerState(pageCount = list.size) 27 | val state = remember { IndicatorState(scope, dotSettings) } 28 | 29 | Column( 30 | Modifier 31 | .fillMaxSize() 32 | .background(Color(0xFFFCFCFF)) 33 | ) { 34 | 35 | HorizontalPager( 36 | state = pagerState, 37 | itemSpacing = 10.dp, 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .padding(0.dp, 10.dp) 41 | ) { page -> 42 | // Our page content 43 | DessertCard(dessert = list[page]) 44 | } 45 | 46 | Indicators( 47 | state, 48 | pagerState, 49 | Modifier 50 | .fillMaxWidth() 51 | .padding(0.dp, 10.dp, 0.dp, 0.dp) 52 | ) 53 | } 54 | } 55 | 56 | @ExperimentalPagerApi 57 | @Composable 58 | fun Indicators(state: IndicatorState, pagerState: PagerState, modifier: Modifier) { 59 | if (pagerState.isScrollInProgress && state.targetPosition != pagerState.targetValue()) { 60 | state.startScrolling(pagerState.targetValue()!!) 61 | } else if (!pagerState.isScrollInProgress) { 62 | state.finishScrolling() 63 | } 64 | emptyIndicators(state = state, modifier = modifier) 65 | filledIndicators(state = state) 66 | } 67 | 68 | 69 | @Composable 70 | fun emptyIndicators(state: IndicatorState, modifier: Modifier) { 71 | var dotSettings = state.dotSettings 72 | Canvas( 73 | modifier = modifier 74 | ) { 75 | state.setFirstIndicatorPosition(center) 76 | for (i in 0 until state.dotSettings.size) { 77 | drawCircle( 78 | color = dotSettings.color, 79 | radius = dotSettings.radius, 80 | center = Offset( 81 | state.firstDotPosition!!.x + dotSettings.distanceBetweenDots * i, 82 | state.firstDotPosition!!.y 83 | ), 84 | alpha = 0.2f 85 | ) 86 | } 87 | } 88 | } 89 | 90 | @ExperimentalPagerApi 91 | @Composable 92 | fun filledIndicators(state: IndicatorState) { 93 | firstFilledDot(state) 94 | secondFilledDot(state) 95 | drawUnion(state) 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.animation.ExperimentalAnimationApi 7 | import androidx.compose.foundation.ExperimentalFoundationApi 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material.* 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.SideEffect 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.dp 19 | import androidx.navigation.NavController 20 | import androidx.navigation.compose.NavHost 21 | import androidx.navigation.compose.composable 22 | import androidx.navigation.compose.rememberNavController 23 | import com.google.accompanist.pager.ExperimentalPagerApi 24 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 25 | import com.represa.draw.ui.BottomBar 26 | import com.represa.draw.ui.DatePicker 27 | import com.represa.draw.ui.RoundIndicators 28 | import com.represa.draw.ui.SplashScreen 29 | 30 | class MainActivity : ComponentActivity() { 31 | @ExperimentalAnimationApi 32 | @ExperimentalFoundationApi 33 | @ExperimentalMaterialApi 34 | @ExperimentalPagerApi 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | setContent { 38 | Surface(color = Color.White, modifier = Modifier.fillMaxSize()) { 39 | MainScreen() 40 | } 41 | 42 | } 43 | } 44 | 45 | @ExperimentalMaterialApi 46 | @ExperimentalPagerApi 47 | @ExperimentalAnimationApi 48 | @ExperimentalFoundationApi 49 | @Composable 50 | fun MainScreen() { 51 | 52 | updateStatusBar() 53 | val navController = rememberNavController() 54 | 55 | NavHost( 56 | navController = navController, 57 | startDestination = "mainScreen", 58 | modifier = Modifier 59 | ) { 60 | composable("mainScreen") { Buttons(navController) } 61 | composable("bottomBar") { BottomBar() } 62 | composable("datePicker") { DatePicker() } 63 | composable("indicators") { DotsIndicator() } 64 | composable("roundIndicators") { RoundIndicators() } 65 | composable("adidasSplash") { SplashScreen {} } 66 | composable("trippyChest") { PhysicoChest() } 67 | } 68 | 69 | 70 | } 71 | 72 | @Composable 73 | fun Buttons(navController: NavController) { 74 | var modifier = Modifier.padding(0.dp, 20.dp) 75 | Column( 76 | modifier = Modifier.fillMaxSize(), 77 | horizontalAlignment = Alignment.CenterHorizontally, 78 | verticalArrangement = Arrangement.Center 79 | ) { 80 | 81 | Button(onClick = { navController.navigate("bottomBar") }, modifier = modifier) { 82 | Text(text = "BottomBar") 83 | } 84 | Button(onClick = { navController.navigate("datePicker") }, modifier = modifier) { 85 | Text(text = "DatePicker") 86 | } 87 | Button(onClick = { navController.navigate("indicators") }, modifier = modifier) { 88 | Text(text = "Indicators") 89 | } 90 | Button(onClick = { navController.navigate("roundIndicators") }, modifier = modifier) { 91 | Text(text = "RoundIndicators") 92 | } 93 | Button(onClick = { navController.navigate("adidasSplash") }, modifier = modifier) { 94 | Text(text = "AdidasSplash") 95 | } 96 | Button(onClick = { navController.navigate("trippyChest") }, modifier = modifier) { 97 | Text(text = "TrippyChest") 98 | } 99 | } 100 | } 101 | 102 | @Composable 103 | fun updateStatusBar() { 104 | val systemUiController = rememberSystemUiController() 105 | val useDarkIcons = !MaterialTheme.colors.isLight 106 | 107 | SideEffect { 108 | systemUiController.setSystemBarsColor( 109 | color = Color.Transparent, 110 | darkIcons = useDarkIcons 111 | ) 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/PhysicoChest.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.runtime.* 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.draw.drawBehind 8 | import androidx.compose.ui.geometry.Offset 9 | import androidx.compose.ui.graphics.* 10 | import androidx.compose.ui.graphics.drawscope.* 11 | import androidx.compose.foundation.background 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.geometry.Size 14 | import kotlinx.coroutines.* 15 | import kotlin.math.* 16 | 17 | @Composable 18 | fun PhysicoChest() { 19 | var scope = rememberCoroutineScope() 20 | var state = remember { ChestState(scope) } 21 | Column( 22 | verticalArrangement = Arrangement.Center, 23 | horizontalAlignment = Alignment.CenterHorizontally 24 | ) { 25 | Chest(state) 26 | } 27 | } 28 | 29 | private val colorLight = Color(0xFFB2EBF2) 30 | private val colorDark = Color(0xFF263238) 31 | private val square = Size(100f, 100f) 32 | 33 | class ChestState(private val scope: CoroutineScope) { 34 | var topLeftProgressHorizontal = Animatable(initialValue = 0f) 35 | var ovalSizeProgressHorizontal = Animatable(initialValue = -90f) 36 | var topLeftProgressVertical = Animatable(initialValue = 0f) 37 | var ovalSizeProgressVertical = Animatable(initialValue = -90f) 38 | var color by mutableStateOf(colorLight) 39 | var drawHorizontal by mutableStateOf(true) 40 | var drawVertical by mutableStateOf(false) 41 | 42 | var horizontalfinish by mutableStateOf(false) 43 | var verticalfinish by mutableStateOf(true) 44 | 45 | private val delay = 3000 46 | private val delay_stop = 1000L 47 | 48 | init { 49 | scope.launch { 50 | delay(delay_stop) 51 | while (isActive) { 52 | startHorizontal().join() 53 | horizontalfinish = true 54 | delay(delay_stop) 55 | drawHorizontal = false 56 | startVertical().join() 57 | verticalfinish = true 58 | delay(delay_stop) 59 | drawVertical = false 60 | } 61 | } 62 | } 63 | 64 | private suspend fun startHorizontal() = scope.launch { 65 | horizontalfinish = false 66 | drawHorizontal = true 67 | color = colorLight 68 | launch { 69 | ovalSizeProgressHorizontal.animateTo( 70 | targetValue = 90f, 71 | animationSpec = tween(durationMillis = delay, easing = LinearEasing) 72 | ) 73 | ovalSizeProgressHorizontal.snapTo(-90f) 74 | } 75 | launch { 76 | topLeftProgressHorizontal.animateTo( 77 | targetValue = 1f, 78 | animationSpec = tween(durationMillis = delay, easing = LinearEasing) 79 | ) 80 | topLeftProgressHorizontal.snapTo(0f) 81 | } 82 | } 83 | 84 | private fun startVertical() = scope.launch { 85 | drawVertical = true 86 | verticalfinish = false 87 | color = colorDark 88 | launch { 89 | ovalSizeProgressVertical.animateTo( 90 | targetValue = 90f, 91 | animationSpec = tween(durationMillis = delay, easing = LinearEasing) 92 | ) 93 | ovalSizeProgressVertical.snapTo(-90f) 94 | } 95 | launch { 96 | topLeftProgressVertical.animateTo( 97 | targetValue = 1f, 98 | animationSpec = tween(durationMillis = delay, easing = LinearEasing) 99 | ) 100 | topLeftProgressVertical.snapTo(0f) 101 | } 102 | } 103 | } 104 | 105 | 106 | @Composable 107 | private fun Chest(state: ChestState) { 108 | 109 | Box( 110 | Modifier 111 | .fillMaxSize() 112 | .background(state.color) 113 | .drawBehind { 114 | 115 | var squareWidthAmount = floor(size.width / square.width) 116 | var restWidth = size.width.rem(square.width) 117 | 118 | var squareHeightAmount = floor(size.height / square.height) 119 | var restHeight = size.height.rem(square.height) 120 | 121 | translate(restWidth / 2, restHeight / 2) { 122 | 123 | var position: Offset 124 | 125 | for (r in 0 until squareHeightAmount.toInt()) { 126 | for (c in 0 until squareWidthAmount.toInt()) { 127 | position = Offset.Zero + Offset(square.width * c, square.height * r) 128 | when { 129 | r % 2 == 1 && c % 2 == 1 && state.drawHorizontal -> { 130 | horizontalLeft(state, position) 131 | } 132 | r % 2 == 1 && c % 2 == 0 && state.drawVertical -> { 133 | verticalDown(state, position) 134 | } 135 | r % 2 == 0 && c % 2 == 1 && state.drawVertical -> { 136 | verticalUp(state, position) 137 | } 138 | r % 2 == 0 && c % 2 == 0 && state.drawHorizontal -> { 139 | horizontalRight(state, position) 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | ) 147 | } 148 | 149 | private fun DrawScope.horizontalRight(state: ChestState, leftCorner: Offset) { 150 | var ovalSizeProgress = state.ovalSizeProgressHorizontal.value 151 | var topLeftProgress = state.topLeftProgressHorizontal.value 152 | var theta = PI.toFloat() * ovalSizeProgress / 180f 153 | 154 | var c2 = Offset(leftCorner.x + square.width * (1 - topLeftProgress), leftCorner.y) 155 | var s2 = Size(square.width * cos(theta), square.height) 156 | var l2 = Offset(c2.x - s2.width / 2, leftCorner.y) 157 | 158 | var c1 = Offset((leftCorner.x + square.width * topLeftProgress), leftCorner.y) 159 | var s1 = Size(square.width * cos(theta), square.height) 160 | var l1 = Offset(c1.x - s1.width / 2, leftCorner.y) 161 | 162 | //Oval from right to left 163 | drawOval( 164 | color = colorDark, 165 | topLeft = l2, 166 | size = s2, 167 | style = Fill 168 | ) 169 | 170 | var centre = when { 171 | c1.x <= c2.x -> c1 172 | else -> c2 173 | } 174 | 175 | drawRect( 176 | color = colorDark, 177 | topLeft = centre, 178 | size = Size((c1.x - c2.x).absoluteValue, square.height) 179 | ) 180 | 181 | //left to right 182 | if (!state.horizontalfinish) { 183 | drawOval( 184 | color = colorLight, 185 | topLeft = l1, 186 | size = s1, 187 | style = Stroke(4f) 188 | ) 189 | } 190 | 191 | drawOval( 192 | color = colorDark, 193 | topLeft = l1, 194 | size = s1, 195 | style = Fill 196 | ) 197 | } 198 | 199 | private fun DrawScope.horizontalLeft(state: ChestState, leftCorner: Offset) { 200 | var animatedProgress = state.ovalSizeProgressHorizontal.value 201 | var sizePRogress = state.topLeftProgressHorizontal.value 202 | var theta = PI.toFloat() * animatedProgress / 180f 203 | 204 | var c2 = Offset(leftCorner.x + square.width * (1 - sizePRogress), leftCorner.y) 205 | var s2 = Size(square.width * cos(theta), square.height) 206 | var l2 = Offset(c2.x - s2.width / 2, leftCorner.y) 207 | 208 | var c1 = Offset((leftCorner.x + square.width * sizePRogress), leftCorner.y) 209 | var s1 = Size(square.width * cos(theta), square.height) 210 | var l1 = Offset(c1.x - s1.width / 2, leftCorner.y) 211 | 212 | //left to right 213 | drawOval( 214 | color = colorDark, 215 | topLeft = l1, 216 | size = s1, 217 | style = Fill 218 | ) 219 | 220 | var centre = when { 221 | c1.x <= c2.x -> c1 222 | else -> c2 223 | } 224 | 225 | drawRect( 226 | color = colorDark, 227 | topLeft = centre, 228 | size = Size((c1.x - c2.x).absoluteValue, square.height) 229 | ) 230 | 231 | //right to left 232 | if (!state.horizontalfinish) { 233 | drawOval( 234 | color = colorLight, 235 | topLeft = l2, 236 | size = s2, 237 | style = Stroke(4f) 238 | ) 239 | } 240 | 241 | drawOval( 242 | color = colorDark, 243 | topLeft = l2, 244 | size = s2, 245 | style = Fill 246 | ) 247 | } 248 | 249 | private fun DrawScope.verticalDown(state: ChestState, leftCorner: Offset) { 250 | var animatedProgress = state.ovalSizeProgressVertical.value 251 | var sizePRogress = state.topLeftProgressVertical.value 252 | var theta = PI.toFloat() * animatedProgress / 180f 253 | 254 | var c2 = Offset(leftCorner.x, (leftCorner.y + square.height * (1 - sizePRogress))) 255 | var s2 = Size(square.width, square.height * cos(theta)) 256 | var l2 = Offset(leftCorner.x, c2.y - s2.height / 2) 257 | 258 | var c1 = Offset(leftCorner.x, leftCorner.y + square.height * sizePRogress) 259 | var s1 = Size(square.width, square.height * cos(theta)) 260 | var l1 = Offset(leftCorner.x, c1.y - s1.height / 2) 261 | 262 | //Oval from bottom to top 263 | drawOval( 264 | color = colorLight, 265 | topLeft = l2, 266 | size = s2, 267 | style = Fill 268 | ) 269 | 270 | var centre = when { 271 | c1.y <= c2.y -> c1 272 | else -> c2 273 | } 274 | 275 | drawRect( 276 | color = colorLight, 277 | topLeft = centre, 278 | size = Size(square.width, (c1.y - c2.y).absoluteValue) 279 | ) 280 | 281 | //Oval from top to bottom 282 | if (!state.verticalfinish) { 283 | drawOval( 284 | color = colorDark, 285 | topLeft = l1, 286 | size = s1, 287 | style = Stroke(4f) 288 | ) 289 | } 290 | 291 | drawOval( 292 | color = colorLight, 293 | topLeft = l1, 294 | size = Size(s1.width - 2, s1.height - 2), 295 | style = Fill 296 | ) 297 | } 298 | 299 | private fun DrawScope.verticalUp(state: ChestState, leftCorner: Offset) { 300 | var animatedProgress = state.ovalSizeProgressVertical.value 301 | var sizePRogress = state.topLeftProgressVertical.value 302 | var theta = PI.toFloat() * animatedProgress / 180f 303 | 304 | var c2 = Offset(leftCorner.x, leftCorner.y + square.height * (1 - sizePRogress)) 305 | var s2 = Size(square.width, square.height * cos(theta)) 306 | var l2 = Offset(leftCorner.x, c2.y - s2.height / 2) 307 | 308 | var c1 = Offset(leftCorner.x, leftCorner.y + square.height * sizePRogress) 309 | var s1 = Size(square.width, square.height * cos(theta)) 310 | var l1 = Offset(leftCorner.x, c1.y - s1.height / 2) 311 | 312 | var centre = when { 313 | c1.y <= c2.y -> c1 314 | else -> c2 315 | } 316 | 317 | drawRect( 318 | color = colorLight, 319 | topLeft = centre, 320 | size = Size(square.width, (c1.y - c2.y).absoluteValue) 321 | ) 322 | 323 | //top to bottom 324 | drawOval( 325 | color = colorLight, 326 | topLeft = l1, 327 | size = s1, 328 | style = Fill 329 | ) 330 | 331 | //Oval from bottom to top 332 | if (!state.verticalfinish) { 333 | drawOval( 334 | color = colorLight, 335 | topLeft = l2, 336 | size = s2, 337 | style = Fill 338 | ) 339 | } 340 | 341 | drawOval( 342 | color = colorDark, 343 | topLeft = l2, 344 | size = s2, 345 | style = Stroke(4f) 346 | ) 347 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/data/Dessert.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.data 2 | 3 | data class Dessert(val url: String, val name: String, val description: String, val price: String) { 4 | 5 | } 6 | 7 | val desserts = mutableListOf( 8 | Dessert("https://images.unsplash.com/photo-1582576601037-b5050b45a44c?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80", 9 | "Strawberry Waffle", 10 | "Eggs, milk, coconout oil, orange, lemon, sugar, cream, syrup (chocolate, maple)", 11 | "$4.50"), 12 | Dessert("https://images.unsplash.com/photo-1562945431-ce2b63d5a7fe?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80", 13 | "Chocolate Donut", 14 | "Eggs, milk, palm oil, lemon, sugar, butter", 15 | "$2"), 16 | Dessert("https://images.unsplash.com/photo-1617806501441-2a4a45c5316c?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80", 17 | "Turkish Baklava", 18 | "Butter, sugar, pistachios, lemon ", 19 | "$0,70/p"), 20 | Dessert("https://images.unsplash.com/photo-1587314168485-3236d6710814?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1570&q=80", 21 | "Strawberry Crepes", 22 | "Eggs, milk, butter, strawberry, lemon, sugar, cream, syrup (chocolate, maple)", 23 | "$3.75") 24 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/extensions/LazyRow.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.extensions 2 | 3 | import androidx.compose.foundation.lazy.LazyListState 4 | import kotlin.math.absoluteValue 5 | 6 | fun LazyListState.getCurrentItem(): Int { 7 | var currentOffset = layoutInfo.viewportEndOffset 8 | var position = -1 9 | layoutInfo.visibleItemsInfo.forEach { 10 | if (it.offset.absoluteValue < currentOffset) { 11 | currentOffset = it.offset.absoluteValue 12 | position = it.index 13 | } 14 | } 15 | return position 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/extensions/Other.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.extensions 2 | 3 | 4 | inline fun safeLet(p1: T1?, p2: T2?, block: (T1, T2) -> R?): R? { 5 | return if (p1 != null && p2 != null) block(p1, p2) else null 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/extensions/PagerState.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.extensions 2 | 3 | import com.google.accompanist.pager.ExperimentalPagerApi 4 | import com.google.accompanist.pager.PagerState 5 | 6 | @ExperimentalPagerApi 7 | fun PagerState.targetValue(): Int { 8 | return if (this.currentPageOffset in -0.15..0.15) { 9 | currentPage 10 | } else { 11 | this.targetPage!! 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/BottomBar.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.animation.core.Animatable 5 | import androidx.compose.animation.core.LinearEasing 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.Canvas 8 | import androidx.compose.foundation.ExperimentalFoundationApi 9 | import androidx.compose.foundation.background 10 | import androidx.compose.foundation.clickable 11 | import androidx.compose.foundation.gestures.animateScrollBy 12 | import androidx.compose.foundation.layout.* 13 | import androidx.compose.foundation.lazy.* 14 | import androidx.compose.foundation.shape.RoundedCornerShape 15 | import androidx.compose.material.Card 16 | import androidx.compose.material.Icon 17 | import androidx.compose.material.Text 18 | import androidx.compose.material.icons.Icons 19 | import androidx.compose.material.icons.filled.* 20 | import androidx.compose.material.icons.outlined.* 21 | import androidx.compose.runtime.* 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.scale 25 | import androidx.compose.ui.geometry.CornerRadius 26 | import androidx.compose.ui.geometry.Offset 27 | import androidx.compose.ui.geometry.Size 28 | import androidx.compose.ui.graphics.Color 29 | import androidx.compose.ui.graphics.drawscope.translate 30 | import androidx.compose.ui.platform.LocalDensity 31 | import androidx.compose.ui.text.font.FontWeight 32 | import androidx.compose.ui.unit.Density 33 | import androidx.compose.ui.unit.dp 34 | import com.represa.draw.extensions.safeLet 35 | import kotlinx.coroutines.CoroutineScope 36 | import kotlinx.coroutines.delay 37 | import kotlinx.coroutines.launch 38 | 39 | @ExperimentalFoundationApi 40 | @ExperimentalAnimationApi 41 | @Composable 42 | fun BottomBar() { 43 | 44 | var items = listOf( 45 | "HOME", 46 | "NEW IN", 47 | "CLOTHING", 48 | "DENIM", 49 | "SHOES", 50 | "ACCESORIES", 51 | "SALE", 52 | "JUNIOR", 53 | "PLUS SIZE" 54 | ) 55 | 56 | var subCategoryItems = listOf( 57 | "ALL CLOTHING", 58 | "SEASON HIGHLIGHT", 59 | "T-SHIRT", 60 | "DENIM", 61 | "VIEW ALL", 62 | "JACKETS", 63 | "JEANS", 64 | "TROUSERS", 65 | "SHORTS" 66 | ) 67 | 68 | val listState = rememberLazyListState() 69 | val gridListState = rememberLazyListState() 70 | 71 | var navigationBarVisibility = remember { mutableStateOf(true) } 72 | val density = LocalDensity.current 73 | 74 | var contentPaddingY = with(density) { 10.dp.toPx() } 75 | var scope = rememberCoroutineScope() 76 | var bottomBarState = remember { 77 | BottomBarState(scope) 78 | } 79 | 80 | Box( 81 | modifier = Modifier 82 | .fillMaxSize() 83 | .background(Color(0xFFFCFCFF)), 84 | contentAlignment = Alignment.BottomStart 85 | ) { 86 | 87 | Background(gridListState, navigationBarVisibility) 88 | 89 | AnimatedVisibility( 90 | visible = navigationBarVisibility.value, 91 | enter = slideInVertically( 92 | // Slide in from 40 dp from the top. 93 | initialOffsetY = { with(density) { 40.dp.roundToPx() } }, 94 | animationSpec = tween(durationMillis = 100, easing = LinearEasing) 95 | ) + fadeIn( 96 | // Fade in with the initial alpha of 0.3f. 97 | initialAlpha = 0.3f 98 | ), 99 | exit = slideOutVertically( 100 | targetOffsetY = { with(density) { 40.dp.roundToPx() } }, 101 | animationSpec = tween(durationMillis = 100, easing = LinearEasing) 102 | 103 | ) + fadeOut( 104 | animationSpec = tween(durationMillis = 100, easing = LinearEasing) 105 | ) 106 | ) { 107 | AnimatedNavigationBar( 108 | listState = listState, 109 | bottomBarState = bottomBarState, 110 | items = items, 111 | subCategoryItems = subCategoryItems, 112 | density = density, 113 | contentPaddingY = contentPaddingY 114 | ) 115 | } 116 | } 117 | } 118 | 119 | @ExperimentalAnimationApi 120 | @Composable 121 | fun AnimatedNavigationBar( 122 | listState: LazyListState, 123 | bottomBarState: BottomBarState, 124 | items: List, 125 | subCategoryItems: List, 126 | density: Density, 127 | contentPaddingY: Float 128 | ) { 129 | Column( 130 | modifier = Modifier 131 | .wrapContentSize() 132 | .background(Color.Transparent), 133 | verticalArrangement = Arrangement.Bottom 134 | ) { 135 | 136 | Column( 137 | modifier = Modifier 138 | .wrapContentSize() 139 | .padding(10.dp, 0.dp), 140 | verticalArrangement = Arrangement.Bottom 141 | ) { 142 | Card( 143 | modifier = Modifier 144 | .fillMaxWidth() 145 | .height(50.dp), 146 | elevation = 10.dp, 147 | shape = RoundedCornerShape(30.dp), 148 | 149 | ) { 150 | Box(modifier = Modifier.fillMaxSize()) { 151 | CategoriesRow( 152 | state = listState, 153 | bottomBarState = bottomBarState, 154 | items = items, 155 | contentPaddingY = contentPaddingY, 156 | density = density 157 | ) 158 | SubCategoryRow( 159 | state = listState, 160 | bottomBarState = bottomBarState, 161 | items = subCategoryItems, 162 | contentPaddingY = contentPaddingY, 163 | density = density 164 | ) 165 | } 166 | } 167 | } 168 | 169 | FakeNavigationBar() 170 | 171 | } 172 | } 173 | 174 | @ExperimentalAnimationApi 175 | @Composable 176 | fun CategoriesRow( 177 | state: LazyListState, 178 | bottomBarState: BottomBarState, 179 | items: List, 180 | contentPaddingY: Float, 181 | density: Density 182 | ) { 183 | 184 | AnimatedVisibility( 185 | visible = bottomBarState.categoriesVisibility, 186 | enter = slideInVertically( 187 | // Slide in from 40 dp from the top. 188 | initialOffsetY = { with(density) { -40.dp.roundToPx() } }, 189 | animationSpec = tween(durationMillis = 300, easing = LinearEasing) 190 | ) + fadeIn( 191 | // Fade in with the initial alpha of 0.3f. 192 | initialAlpha = 0.3f 193 | ), 194 | exit = slideOutVertically( 195 | animationSpec = tween(durationMillis = 300, easing = LinearEasing) 196 | 197 | ) + fadeOut( 198 | animationSpec = tween(durationMillis = 300, easing = LinearEasing) 199 | ) 200 | ) { 201 | 202 | DrawIndicator(state, bottomBarState, contentPaddingY, false, density) 203 | Categories(state, bottomBarState, items, false) 204 | } 205 | } 206 | 207 | @ExperimentalAnimationApi 208 | @Composable 209 | fun SubCategoryRow( 210 | state: LazyListState, 211 | bottomBarState: BottomBarState, 212 | items: List, 213 | contentPaddingY: Float, 214 | density: Density 215 | ) { 216 | 217 | AnimatedVisibility( 218 | visible = bottomBarState.subCategoriesVisibility, 219 | enter = slideInVertically( 220 | // Slide in from 40 dp from the top. 221 | initialOffsetY = { with(density) { 40.dp.roundToPx() } }, 222 | animationSpec = tween(durationMillis = 300, easing = LinearEasing) 223 | ) + fadeIn( 224 | // Fade in with the initial alpha of 0.3f. 225 | initialAlpha = 0.3f 226 | ), 227 | exit = slideOutVertically( 228 | targetOffsetY = { with(density) { 40.dp.roundToPx() } }, 229 | animationSpec = tween(durationMillis = 300, easing = LinearEasing) 230 | 231 | ) + fadeOut( 232 | animationSpec = tween(durationMillis = 300, easing = LinearEasing) 233 | ) 234 | ) { 235 | 236 | Box() { 237 | DrawIndicator(state, bottomBarState, contentPaddingY, true, density) 238 | Categories(state, bottomBarState, items, true) 239 | } 240 | 241 | Box(modifier = Modifier.wrapContentSize()) { 242 | Box( 243 | modifier = Modifier 244 | .fillMaxHeight() 245 | .width(25.dp) 246 | .background(Color.White) 247 | ) 248 | Card( 249 | modifier = Modifier 250 | .fillMaxHeight() 251 | .width(40.dp) 252 | .padding(10.dp, 10.dp, 0.dp, 10.dp), 253 | elevation = 5.dp, 254 | shape = RoundedCornerShape(30.dp), 255 | backgroundColor = Color.LightGray, 256 | ) { 257 | Icon(imageVector = Icons.Default.ArrowBack, 258 | contentDescription = "", 259 | modifier = Modifier 260 | .scale(0.6f) 261 | .clickable { 262 | bottomBarState.reset(state) 263 | } 264 | ) 265 | } 266 | } 267 | 268 | } 269 | 270 | } 271 | 272 | /** 273 | * @contentPadding -> Represent the margin of our Box(), needed to show properly the blue indicator 274 | */ 275 | @Composable 276 | fun DrawIndicator( 277 | state: LazyListState, 278 | bottomBarState: BottomBarState, 279 | contentPaddingY: Float, 280 | subCategory: Boolean, 281 | density: Density 282 | ) { 283 | //When we show the back arrow, we have to count the arrow size in order to show properly the 284 | //items from the correct start (The bac arrow view is 40.dp) 285 | var backArrowOffset = if (subCategory) { 286 | with(density) { 40.dp.toPx() } 287 | } else { 288 | 0f 289 | } 290 | 291 | var height = with(density) { 30.dp.toPx() } 292 | var paddingBetweenObjects = with(density) { 10.dp.roundToPx().toFloat() } 293 | 294 | //In order to get the proper offset from the item 295 | //We have to add to the item.offset the contentPadding of the LazyRow in the axisX 296 | var contentPaddingX = if (subCategory) { 297 | with(density) { 20.dp.toPx() } 298 | } else { 299 | with(density) { 10.dp.toPx() } 300 | } 301 | 302 | //Offset to move the Canvas center (0,0) to there 303 | // (25,0) -> half of the icon 304 | var center = if (subCategory) { 305 | Offset(with(density) { 25.dp.toPx() }, 0f) 306 | } else { 307 | Offset(with(density) { 0.dp.toPx() }, 0f) 308 | } 309 | 310 | Canvas(modifier = Modifier) { 311 | translate(center.x, center.y) { 312 | when (bottomBarState.animationState) { 313 | AnimationState.SCROLLING -> { 314 | with(bottomBarState) { 315 | state.getDistance(previousIndex, currentIndex)?.let { distance -> 316 | drawRoundRect( 317 | color = Color.Blue, 318 | topLeft = Offset( 319 | state.getTopLeftAxisX( 320 | previousIndex, 321 | contentPaddingX 322 | ) + (distance * animation.value), 323 | contentPaddingY 324 | ), 325 | size = state.getSize(currentIndex, paddingBetweenObjects, height), 326 | cornerRadius = CornerRadius(40f) 327 | ) 328 | } ?: run { 329 | 330 | var to = state.getItem(currentIndex) 331 | 332 | var topLeft = when { 333 | currentIndex > previousIndex -> { 334 | Offset( 335 | (to!! 336 | .offset + contentPaddingX) * animation.value, 337 | contentPaddingY 338 | ) 339 | } 340 | currentIndex < previousIndex -> { 341 | Offset( 342 | state.layoutInfo.viewportEndOffset - (state.layoutInfo.viewportEndOffset - to!!.offset - contentPaddingY) * (animation.value) + backArrowOffset, 343 | contentPaddingY 344 | ) 345 | } 346 | else -> { 347 | Offset(to!!.offset.toFloat(), contentPaddingY) 348 | } 349 | } 350 | drawRoundRect( 351 | color = Color.Blue, 352 | topLeft = topLeft, 353 | size = state.getSize(currentIndex, paddingBetweenObjects, height), 354 | cornerRadius = CornerRadius(40f) 355 | ) 356 | } 357 | } 358 | } 359 | AnimationState.IDLE -> { 360 | with(bottomBarState) { 361 | //if(subCategory && ) 362 | drawRoundRect( 363 | color = Color.Blue, 364 | topLeft = Offset( 365 | state.getTopLeftAxisX( 366 | currentIndex, 367 | contentPaddingX 368 | ), 369 | contentPaddingY 370 | ), 371 | size = state.getSize(currentIndex, paddingBetweenObjects, height), 372 | cornerRadius = CornerRadius(40f) 373 | ) 374 | } 375 | } 376 | } 377 | } 378 | } 379 | } 380 | 381 | @ExperimentalAnimationApi 382 | @Composable 383 | fun Categories( 384 | state: LazyListState, 385 | bottomBarState: BottomBarState, 386 | items: List, 387 | subCategory: Boolean 388 | ) { 389 | 390 | var scope = rememberCoroutineScope() 391 | var padding = if (subCategory) { 392 | PaddingValues(25.dp, 0.dp, 0.dp, 0.dp) 393 | } else { 394 | PaddingValues(0.dp, 0.dp) 395 | } 396 | var paddingValues = if (subCategory) { 397 | PaddingValues(20.dp, 0.dp) 398 | } else { 399 | PaddingValues(10.dp, 0.dp) 400 | } 401 | 402 | LazyRow( 403 | modifier = Modifier.padding(padding), 404 | contentPadding = paddingValues, 405 | state = state 406 | ) { 407 | itemsIndexed(items) { index, item -> 408 | Box( 409 | modifier = Modifier 410 | .fillMaxHeight() 411 | .wrapContentWidth() 412 | .padding(0.dp, 0.dp, 10.dp, 0.dp) 413 | .clickable { 414 | state 415 | .getItem(index) 416 | ?.let { item -> 417 | var positionFromMiddle = when { 418 | state.layoutInfo.viewportEndOffset / 2 < item.offset + item.size -> { 419 | Position.RIGHT 420 | } 421 | state.layoutInfo.viewportEndOffset / 2 > item.offset + item.size -> { 422 | Position.LEFT 423 | } 424 | else -> { 425 | Position.IDLE 426 | } 427 | } 428 | scope.launch { 429 | state.animateScrollBy( 430 | state.toScroll( 431 | index, 432 | positionFromMiddle, 433 | bottomBarState, 434 | state 435 | ) 436 | ) 437 | } 438 | } 439 | 440 | }, 441 | contentAlignment = Alignment.Center 442 | ) { 443 | Text( 444 | text = item, modifier = Modifier 445 | .padding(20.dp, 0.dp), 446 | fontWeight = FontWeight.Bold, 447 | color = if (bottomBarState.currentIndex == index) Color.White else Color.Black 448 | ) 449 | } 450 | } 451 | 452 | } 453 | 454 | } 455 | 456 | @ExperimentalFoundationApi 457 | @Composable 458 | fun Background(gridListState: LazyListState, navigationBarVisibility: MutableState) { 459 | 460 | var offset = remember { mutableStateOf(0) } 461 | var first = remember { mutableStateOf(0) } 462 | gridListState.setScrollBehaviour(first, offset, navigationBarVisibility) 463 | 464 | LazyVerticalGrid( 465 | state = gridListState, 466 | cells = GridCells.Fixed(2), 467 | modifier = Modifier 468 | .fillMaxSize(), 469 | contentPadding = PaddingValues(13.dp, 10.dp) 470 | ) { 471 | items(50) { item -> 472 | var padding = if (item % 2 == 0) { 473 | PaddingValues(0.dp, 0.dp, 3.dp, 6.dp) 474 | } else { 475 | PaddingValues(3.dp, 0.dp, 3.dp, 6.dp) 476 | } 477 | Box( 478 | modifier = Modifier 479 | .height(190.dp) 480 | .padding(padding) 481 | ) { 482 | Box( 483 | modifier = Modifier 484 | .fillMaxSize() 485 | .background(Color(0xFFeeeeee)) 486 | ) { 487 | 488 | } 489 | } 490 | } 491 | 492 | } 493 | } 494 | 495 | private fun LazyListState.toScroll( 496 | index: Int, 497 | positionFromMiddle: Position, 498 | bottomBarState: BottomBarState, 499 | state: LazyListState 500 | ): Float { 501 | getItem(index)?.let { item -> 502 | return when (positionFromMiddle) { 503 | Position.RIGHT -> { 504 | bottomBarState.scroll(index, state) 505 | item.offset - (layoutInfo.viewportEndOffset / 2f) + item.size / 2 506 | } 507 | Position.LEFT -> { 508 | bottomBarState.scroll(index, state) 509 | item.size / 2 + item.offset - (layoutInfo.viewportEndOffset / 2f) 510 | } 511 | Position.IDLE -> 0f 512 | } 513 | } ?: kotlin.run { 514 | return@toScroll 0f 515 | } 516 | } 517 | 518 | private fun LazyListState.getItem(index: Int): LazyListItemInfo? { 519 | return layoutInfo.visibleItemsInfo.firstOrNull() { 520 | it.index == index 521 | } 522 | } 523 | 524 | private fun LazyListState.getTopLeftAxisX( 525 | index: Int, 526 | contentPaddingX: Float = 0f 527 | ): Float { 528 | var item = getItem(index) 529 | item?.let { 530 | return item.offset.toFloat() + contentPaddingX 531 | } ?: run { 532 | return 0f 533 | } 534 | } 535 | 536 | private fun LazyListState.getDistance(from: Int, to: Int): Int? { 537 | var from = getItem(from) 538 | var to = getItem(to) 539 | safeLet(from, to) { from, to -> 540 | return to.offset - from.offset 541 | } ?: run { 542 | return null 543 | } 544 | } 545 | 546 | private fun LazyListState.getSize( 547 | index: Int, 548 | contentPadding: Float = 0f, 549 | height: Float = 0f 550 | ): Size { 551 | var item = getItem(index) 552 | item?.let { 553 | return Size(item.size.toFloat() - contentPadding, height) 554 | } ?: run { 555 | return Size.Zero 556 | } 557 | } 558 | 559 | private fun LazyListState.setScrollBehaviour( 560 | first: MutableState, 561 | offset: MutableState, 562 | navigationBarVisibility: MutableState 563 | ) { 564 | if (isScrollInProgress) { 565 | layoutInfo.visibleItemsInfo.firstOrNull()?.let { firstItem -> 566 | when { 567 | firstItem.index > first.value -> { 568 | navigationBarVisibility.value = false 569 | offset.value = 0 570 | } 571 | firstItem.index < first.value -> { 572 | navigationBarVisibility.value = true 573 | offset.value = firstItem.offset 574 | } 575 | else -> { 576 | when { 577 | firstItem.offset < offset.value -> { 578 | navigationBarVisibility.value = false 579 | } 580 | firstItem.offset > offset.value -> { 581 | navigationBarVisibility.value = true 582 | } 583 | } 584 | offset.value = firstItem.offset 585 | } 586 | } 587 | first.value = firstItem.index 588 | } 589 | } 590 | } 591 | 592 | enum class Position { 593 | RIGHT, 594 | LEFT, 595 | IDLE 596 | } 597 | 598 | enum class AnimationState { 599 | SCROLLING, 600 | IDLE 601 | } 602 | 603 | class BottomBarState(var scope: CoroutineScope) { 604 | 605 | var animation = Animatable(initialValue = 0f) 606 | var previousIndex = 0 607 | var currentIndex by mutableStateOf(0) 608 | var animationState by mutableStateOf(AnimationState.IDLE) 609 | 610 | var categoriesVisibility by mutableStateOf(true) 611 | var subCategoriesVisibility by mutableStateOf(false) 612 | 613 | 614 | fun scroll(index: Int, state: LazyListState) { 615 | previousIndex = currentIndex 616 | currentIndex = index 617 | if (categoriesVisibility && !subCategoriesVisibility) { 618 | scope.launch { 619 | animationState = AnimationState.SCROLLING 620 | animation.animateTo( 621 | targetValue = 1f, 622 | animationSpec = tween(durationMillis = 150, easing = LinearEasing) 623 | ) 624 | categoriesVisibility = !categoriesVisibility 625 | delay(350) 626 | subCategoriesVisibility = !subCategoriesVisibility 627 | animationState = AnimationState.IDLE 628 | animation.snapTo(0f) 629 | previousIndex = 0 630 | currentIndex = 0 631 | state.scrollToItem(0) 632 | } 633 | } else if (!categoriesVisibility && subCategoriesVisibility) { 634 | scope.launch { 635 | animationState = AnimationState.SCROLLING 636 | animation.animateTo( 637 | targetValue = 1f, 638 | animationSpec = tween(durationMillis = 150, easing = LinearEasing) 639 | ) 640 | animationState = AnimationState.IDLE 641 | animation.snapTo(0f) 642 | } 643 | } 644 | } 645 | 646 | fun reset(state: LazyListState) { 647 | scope.launch { 648 | subCategoriesVisibility = !subCategoriesVisibility 649 | delay(300) 650 | categoriesVisibility = !categoriesVisibility 651 | animationState = AnimationState.IDLE 652 | animation.snapTo(0f) 653 | previousIndex = 0 654 | currentIndex = 0 655 | state.scrollToItem(0) 656 | } 657 | } 658 | 659 | } 660 | 661 | @Composable 662 | fun FakeNavigationBar() { 663 | Row( 664 | modifier = Modifier 665 | .fillMaxWidth() 666 | .height(64.dp) 667 | .padding(0.dp, 8.dp, 0.dp, 0.dp) 668 | .background(Color.White) 669 | ) { 670 | for (i in 0..4) { 671 | var (icon, title) = 672 | when (i) { 673 | 0 -> Pair(Icons.Outlined.Home, "Home") 674 | 1 -> Pair(Icons.Outlined.Search, "Search") 675 | 2 -> Pair(Icons.Outlined.FavoriteBorder, "Favourites") 676 | 3 -> Pair(Icons.Outlined.Face, "My Account") 677 | 4 -> Pair(Icons.Outlined.Settings, "Settings") 678 | else -> Pair(Icons.Outlined.Settings, "Settings") 679 | } 680 | Column( 681 | modifier = Modifier 682 | .weight(1f) 683 | .fillMaxSize(), 684 | verticalArrangement = Arrangement.Center, 685 | horizontalAlignment = Alignment.CenterHorizontally 686 | ) { 687 | Icon( 688 | imageVector = icon, 689 | contentDescription = "", 690 | tint = if (i == 0) { 691 | Color.DarkGray 692 | } else { 693 | Color.LightGray 694 | } 695 | ) 696 | Text( 697 | text = title, 698 | color = if (i == 0) { 699 | Color.DarkGray 700 | } else { 701 | Color.LightGray 702 | } 703 | ) 704 | } 705 | } 706 | 707 | } 708 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/DatePicker.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.gestures.detectVerticalDragGestures 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.rememberScrollState 9 | import androidx.compose.foundation.shape.CircleShape 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.foundation.verticalScroll 12 | import androidx.compose.material.* 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.draw.drawBehind 18 | import androidx.compose.ui.geometry.Offset 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.geometry.Size 21 | import androidx.compose.ui.graphics.* 22 | import androidx.compose.ui.input.pointer.pointerInput 23 | import androidx.compose.ui.layout.onGloballyPositioned 24 | import androidx.compose.ui.text.TextStyle 25 | import androidx.compose.ui.text.font.FontStyle 26 | import androidx.compose.ui.text.font.FontWeight 27 | import androidx.compose.ui.text.style.TextAlign 28 | import androidx.compose.ui.text.style.TextDecoration 29 | import androidx.compose.ui.unit.sp 30 | import kotlinx.coroutines.CoroutineScope 31 | import kotlinx.coroutines.delay 32 | 33 | @ExperimentalMaterialApi 34 | @Composable 35 | fun DatePicker() { 36 | var coroutineScope = rememberCoroutineScope() 37 | var calendarData = remember { CalendarData(coroutineScope) } 38 | 39 | val bottomSheetScaffoldState = rememberBottomSheetScaffoldState() 40 | 41 | BoxWithConstraints() { 42 | BottomSheetScaffold(scaffoldState = bottomSheetScaffoldState, 43 | sheetShape = RoundedCornerShape(20.dp), 44 | sheetPeekHeight = maxHeight * 0.7f, 45 | sheetContent = { 46 | Column( 47 | modifier = Modifier 48 | .fillMaxWidth() 49 | .fillMaxHeight() 50 | .verticalScroll(rememberScrollState()) 51 | .padding(25.dp, 10.dp), 52 | verticalArrangement = Arrangement.Top 53 | ) { 54 | Calendar(calendarData) 55 | } 56 | }, 57 | modifier = Modifier.onGloballyPositioned { 58 | 59 | }) { 60 | var brush = Brush.sweepGradient( 61 | listOf( 62 | Color.Blue, 63 | Color.Cyan, 64 | Color(0xFF66ffff), 65 | Color(0xFF0099ff), 66 | Color(0xFF0066ff), 67 | Color.Blue 68 | ) 69 | ) 70 | 71 | BoxWithConstraints( 72 | modifier = Modifier 73 | .fillMaxSize() 74 | .background(brush) 75 | ) { 76 | Column( 77 | modifier = Modifier 78 | .padding(20.dp, 0.dp) 79 | .height(maxHeight * 0.28f), 80 | verticalArrangement = Arrangement.Bottom 81 | ) { 82 | 83 | Text( 84 | text = "When do you want \nto travel?", 85 | fontSize = 30.sp, 86 | color = Color.White, 87 | fontWeight = FontWeight.SemiBold, 88 | ) 89 | } 90 | } 91 | } 92 | 93 | } 94 | } 95 | 96 | @Composable 97 | fun Calendar(calendarData: CalendarData) { 98 | Decoration() 99 | Box(Modifier.height(5.dp)) 100 | Weeks(calendarData) 101 | } 102 | 103 | @Composable 104 | fun Decoration() { 105 | Indicator() 106 | Month() 107 | WeekDays() 108 | Separator() 109 | } 110 | 111 | @Composable 112 | fun Weeks(calendarData: CalendarData) { 113 | calendarData.apply { 114 | monthDays.keys.forEach { week -> 115 | Row() { 116 | monthDays[week]!!.forEach { day -> 117 | Day(day, calendarData, Modifier.weight(1f)) 118 | } 119 | var daysInWeek = monthDays.getValue(week).size 120 | if (daysInWeek < 7) { 121 | Box(modifier = Modifier.weight((7 - daysInWeek).toFloat())) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | 129 | @Composable 130 | fun Day(day: Int, calendarData: CalendarData, modifier: Modifier) { 131 | 132 | var currentState by remember { 133 | mutableStateOf(CircleDayState.IDLE) 134 | } 135 | val transition = updateTransition(currentState, label = "") 136 | 137 | val circleSize by transition.animateFloat( 138 | transitionSpec = { 139 | when { 140 | CircleDayState.IDLE isTransitioningTo CircleDayState.Selected -> 141 | tween(1000) 142 | else -> 143 | tween(1000) 144 | } 145 | } 146 | ) { state -> 147 | when (state) { 148 | CircleDayState.IDLE -> 0f 149 | CircleDayState.Selected -> 0.4f 150 | } 151 | } 152 | 153 | Card( 154 | modifier = modifier.height(40.dp), 155 | elevation = 0.dp, 156 | shape = RoundedCornerShape(0.dp) 157 | ) { 158 | Box( 159 | contentAlignment = Alignment.Center, 160 | modifier = Modifier 161 | .fillMaxSize() 162 | .clickable { 163 | currentState = if (currentState == CircleDayState.IDLE) { 164 | CircleDayState.Selected 165 | } else { 166 | CircleDayState.IDLE 167 | } 168 | } 169 | .drawBehind { 170 | drawRect( 171 | color = Color.LightGray, 172 | alpha = circleSize, 173 | size = Size(size.width, size.height) 174 | ) 175 | } 176 | ) { 177 | Square(day, calendarData) 178 | Circle(day, calendarData) 179 | Text( 180 | text = day.toString(), 181 | color = if (calendarData.startDay == day || calendarData.endDay == day) Color.White else Color.Black, 182 | fontWeight = FontWeight.SemiBold 183 | ) 184 | } 185 | } 186 | } 187 | 188 | 189 | @Composable 190 | fun Circle(day: Int, calendarData: CalendarData) { 191 | var currentState by remember { 192 | mutableStateOf(CircleDayState.IDLE) 193 | } 194 | val transition = updateTransition(currentState, label = "") 195 | 196 | val circleSize by transition.animateFloat( 197 | transitionSpec = { 198 | when { 199 | CircleDayState.IDLE isTransitioningTo CircleDayState.Selected -> 200 | spring(stiffness = 200f) 201 | else -> 202 | spring(stiffness = 200f) 203 | } 204 | } 205 | ) { state -> 206 | when (state) { 207 | CircleDayState.IDLE -> 0f 208 | CircleDayState.Selected -> 1f 209 | } 210 | } 211 | 212 | if (calendarData.startDay == day || calendarData.endDay == day) { 213 | currentState = CircleDayState.Selected 214 | } else if (calendarData.startDay != day && calendarData.endDay != day) { 215 | currentState = CircleDayState.IDLE 216 | } 217 | 218 | Box(contentAlignment = Alignment.Center, 219 | modifier = Modifier 220 | .fillMaxSize() 221 | .drawBehind { 222 | drawCircle( 223 | color = Color.Black, 224 | radius = circleSize * minOf(size.height, size.width) / 2, 225 | center = center 226 | ) 227 | 228 | } 229 | .clickable { 230 | calendarData.click(day) 231 | }) { 232 | } 233 | } 234 | 235 | @Composable 236 | private fun Square(day: Int, calendarData: CalendarData) { 237 | var currentState by remember { 238 | mutableStateOf(SquareDayState.IDLE) 239 | } 240 | val transition = updateTransition(currentState, label = "2") 241 | 242 | val alpha by transition.animateFloat( 243 | transitionSpec = { 244 | when { 245 | SquareDayState.IDLE isTransitioningTo SquareDayState.Filled -> 246 | tween(600) 247 | else -> 248 | tween(500) 249 | } 250 | } 251 | ) { state -> 252 | when (state) { 253 | SquareDayState.IDLE -> 0f 254 | SquareDayState.Filled -> 0.4f 255 | } 256 | } 257 | 258 | currentState = if (calendarData.filledDays.contains(day)) { 259 | SquareDayState.Filled 260 | } else { 261 | SquareDayState.IDLE 262 | } 263 | 264 | Box( 265 | modifier = Modifier 266 | .fillMaxSize() 267 | .drawBehind { 268 | if (calendarData.filledDays.contains(day)) { 269 | var border = calendarData.border(day) 270 | if (border == BorderState.MIDDLE) { 271 | drawRect( 272 | color = Color.LightGray, 273 | alpha = alpha, 274 | size = if (calendarData.startDay == day || calendarData.endDay == day) { 275 | Size(size.width / 2, size.height) 276 | } else { 277 | Size(size.width, size.height) 278 | }, 279 | topLeft = if (calendarData.startDay == day) { 280 | Offset(size.width / 2, 0f) 281 | } else { 282 | Offset.Zero 283 | } 284 | ) 285 | } else { 286 | var brush = if (border == BorderState.LEFT) { 287 | Brush.horizontalGradient( 288 | colors = listOf( 289 | Color.LightGray, 290 | Color.Transparent 291 | ), 292 | endX = 0f, 293 | startX = size.width 294 | ) 295 | } else { 296 | Brush.horizontalGradient( 297 | colors = listOf( 298 | Color.LightGray, 299 | Color.Transparent 300 | ) 301 | ) 302 | } 303 | drawRect( 304 | brush = brush, 305 | alpha = alpha, 306 | size = if (calendarData.startDay == day || calendarData.endDay == day) { 307 | Size(size.width / 2, size.height) 308 | } else { 309 | Size(size.width, size.height) 310 | }, 311 | topLeft = if (calendarData.startDay == day) { 312 | Offset(size.width / 2, 0f) 313 | } else { 314 | Offset.Zero 315 | } 316 | ) 317 | } 318 | } 319 | } 320 | ) 321 | } 322 | 323 | 324 | @Composable 325 | fun Indicator() { 326 | Row( 327 | horizontalArrangement = Arrangement.Center, 328 | modifier = Modifier.fillMaxWidth() 329 | ) { 330 | Box( 331 | modifier = Modifier 332 | .clip(CircleShape) 333 | .width(70.dp) 334 | .height(5.dp) 335 | .background(Color.LightGray) 336 | ) 337 | } 338 | } 339 | 340 | @Composable 341 | fun Month() { 342 | Text( 343 | text = "June 21", 344 | fontWeight = FontWeight.Bold, 345 | modifier = Modifier.padding(14.dp, 20.dp, 0.dp, 0.dp), 346 | fontSize = 18.sp 347 | ) 348 | } 349 | 350 | @Composable 351 | fun WeekDays() { 352 | var days = listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") 353 | Row(modifier = Modifier.padding(0.dp, 25.dp, 0.dp, 10.dp)) { 354 | days.forEach { day -> 355 | Text( 356 | text = day, 357 | modifier = Modifier.weight(1f), 358 | textAlign = TextAlign.Center, 359 | color = Color.Gray 360 | ) 361 | } 362 | } 363 | } 364 | 365 | @Composable 366 | fun Separator() { 367 | Box( 368 | modifier = Modifier 369 | .fillMaxWidth() 370 | .height(1.dp) 371 | .background(Color(0xFFEEEEEE)) 372 | ) 373 | } 374 | 375 | private enum class CircleDayState { 376 | Selected, 377 | IDLE 378 | } 379 | 380 | private enum class SquareDayState { 381 | Filled, 382 | IDLE 383 | } 384 | 385 | enum class BorderState { 386 | MIDDLE, LEFT, RIGHT 387 | } 388 | 389 | class CalendarData(private val scope: CoroutineScope) { 390 | var filledDays = mutableStateListOf() 391 | var startDay by mutableStateOf(null) 392 | var endDay by mutableStateOf(null) 393 | var nextEndDay: Boolean = false 394 | var monthDays: Map> = mapOf( 395 | (1 to listOf(1, 2, 3, 4, 5, 6, 7)), 396 | (2 to listOf(8, 9, 10, 11, 12, 13, 14)), 397 | (3 to listOf(15, 16, 17, 18, 19, 20, 21)), 398 | (4 to listOf(22, 23, 24, 25, 26, 27, 28)), 399 | (5 to listOf(29, 30, 31)) 400 | ) 401 | 402 | fun fill() { 403 | var list = mutableListOf() 404 | for (i in startDay!!..endDay!!) { 405 | list.add(i) 406 | } 407 | filledDays.addAll(list) 408 | } 409 | 410 | fun click(day: Int) { 411 | if (nextEndDay) { 412 | if (day > startDay!!) { 413 | endDay = day 414 | nextEndDay = false 415 | if (filledDays.isEmpty()) { 416 | fill() 417 | } 418 | } else { 419 | startDay(day) 420 | } 421 | } else { 422 | startDay(day) 423 | } 424 | } 425 | 426 | fun startDay(day: Int) { 427 | startDay = day 428 | endDay = null 429 | filledDays.clear() 430 | nextEndDay = true 431 | } 432 | 433 | fun border(day: Int): BorderState { 434 | var border = BorderState.MIDDLE 435 | monthDays.keys.forEach { week -> 436 | if (monthDays.getValue(week).contains(day)) { 437 | if (monthDays.getValue(week).indexOf(day) == 0) { 438 | border = BorderState.LEFT 439 | } else if (monthDays.getValue(week) 440 | .indexOf(day) == monthDays.getValue(week).size - 1 441 | ) { 442 | border = BorderState.RIGHT 443 | } 444 | } 445 | } 446 | return border 447 | } 448 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/DessertCard.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.Card 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.layout.ContentScale 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import coil.compose.rememberImagePainter 17 | import com.represa.draw.data.Dessert 18 | 19 | @Composable 20 | fun DessertCard(dessert: Dessert) { 21 | Card( 22 | Modifier 23 | .padding(0.dp, 10.dp) 24 | .width(300.dp) 25 | .height(320.dp), 26 | elevation = 3.dp, 27 | shape = RoundedCornerShape(7.dp) 28 | ) { 29 | Column { 30 | Image( 31 | painter = rememberImagePainter( 32 | data = dessert.url 33 | ), 34 | contentDescription = "f", 35 | contentScale = ContentScale.FillBounds, 36 | modifier = Modifier.height(200.dp) 37 | ) 38 | Column(Modifier.padding(10.dp)) { 39 | Text( 40 | text = dessert.name, 41 | fontSize = 20.sp, 42 | fontWeight = FontWeight.Medium 43 | ) 44 | Text( 45 | text = dessert.description, 46 | fontSize = 15.sp, 47 | modifier = Modifier.padding(0.dp, 10.dp), 48 | color = Color.Gray 49 | ) 50 | Row( 51 | verticalAlignment = Alignment.Bottom, 52 | modifier = Modifier.fillMaxHeight() 53 | ) { 54 | Text(text = dessert.price) 55 | } 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/Indicators.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.Canvas 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.geometry.Offset 14 | import androidx.compose.ui.geometry.Size 15 | import androidx.compose.ui.graphics.Color 16 | import com.google.accompanist.pager.ExperimentalPagerApi 17 | import kotlinx.coroutines.CoroutineScope 18 | import kotlinx.coroutines.launch 19 | import kotlin.math.absoluteValue 20 | 21 | @ExperimentalPagerApi 22 | @Composable 23 | fun firstFilledDot(state: IndicatorState) { 24 | if (state.stateFirstDot == IndicatorState.DotState.SCROLLING) { 25 | Canvas( 26 | Modifier.fillMaxWidth() 27 | ) { 28 | var targetPosition = state.getOffset(state.targetPosition) 29 | var currentPosition = state.getOffset(state.currentPosition) 30 | var distanceDots = (targetPosition.x - currentPosition.x) 31 | 32 | //This is gonna be the first filled dot 33 | var firstDotAnimated = Offset( 34 | currentPosition.x + (distanceDots * state.animation.value), 35 | state.firstDotPosition!!.y 36 | ) 37 | drawCircle( 38 | color = Color.Black, 39 | radius = state.dotSettings.radius, 40 | center = firstDotAnimated, 41 | alpha = 0.8f 42 | ) 43 | } 44 | } else { 45 | Canvas( 46 | Modifier.fillMaxWidth() 47 | ) { 48 | var currentPosition = state.getOffset(state.currentPosition) 49 | 50 | //This is gonna be the first filled dot 51 | var dot = Offset( 52 | currentPosition.x, 53 | state.firstDotPosition!!.y 54 | ) 55 | drawCircle( 56 | color = Color.Black, 57 | radius = state.dotSettings.radius, 58 | center = dot, 59 | alpha = 0.8f 60 | ) 61 | } 62 | } 63 | } 64 | 65 | @Composable 66 | fun secondFilledDot(state: IndicatorState) { 67 | if (state.stateSecondDot == IndicatorState.DotState.SCROLLING) { 68 | 69 | Canvas( 70 | Modifier.fillMaxWidth() 71 | ) { 72 | var targetPosition = state.getOffset(state.targetPosition) 73 | var currentPosition = state.getOffset(state.currentPosition) 74 | var distanceDots = (targetPosition.x - currentPosition.x) 75 | 76 | //This is gonna be the first filled dot 77 | var secondDotAnimated = Offset( 78 | currentPosition.x + (distanceDots * state.animationSecond.value), 79 | state.firstDotPosition!!.y 80 | ) 81 | drawCircle( 82 | color = Color.Black, 83 | radius = state.dotSettings.radius, 84 | center = secondDotAnimated, 85 | alpha = 0.8f 86 | ) 87 | } 88 | } else { 89 | Canvas( 90 | Modifier.fillMaxWidth() 91 | ) { 92 | var currentPosition = state.getOffset(state.currentPosition) 93 | 94 | //This is gonna be the first filled dot 95 | var dot = Offset( 96 | currentPosition.x, 97 | state.firstDotPosition!!.y 98 | ) 99 | drawCircle( 100 | color = Color.Black, 101 | radius = state.dotSettings.radius, 102 | center = dot, 103 | alpha = 0.8f 104 | ) 105 | } 106 | } 107 | } 108 | 109 | @Composable 110 | fun drawUnion(state: IndicatorState) { 111 | if (state.stateFirstDot == IndicatorState.DotState.SCROLLING) { 112 | Canvas( 113 | Modifier.fillMaxWidth() 114 | ) { 115 | var targetPosition = state.getOffset(state.targetPosition) 116 | var currentPosition = state.getOffset(state.currentPosition) 117 | var distanceDot = (targetPosition.x - currentPosition.x) 118 | 119 | var firstDotAnimated = Offset( 120 | currentPosition.x + (distanceDot * state.animation.value), 121 | state.firstDotPosition!!.y 122 | ) 123 | var secondDotAnimated = Offset( 124 | currentPosition.x + (distanceDot * state.animationSecond.value), 125 | state.firstDotPosition!!.y 126 | ) 127 | //This gonna be the rectangle between filled dots 128 | var topleft = if (secondDotAnimated.x <= firstDotAnimated.x) { 129 | Offset( 130 | secondDotAnimated.x, 131 | state.firstDotPosition!!.y - state.dotSettings.radius 132 | ) 133 | } else { 134 | Offset( 135 | firstDotAnimated.x, 136 | state.firstDotPosition!!.y - state.dotSettings.radius 137 | ) 138 | } 139 | drawRect( 140 | color = Color.Black, 141 | alpha = 0.8f, 142 | topLeft = topleft, 143 | size = Size( 144 | (secondDotAnimated.x - firstDotAnimated.x).absoluteValue, 145 | state.dotSettings.radius * 2 146 | ) 147 | ) 148 | } 149 | } 150 | } 151 | 152 | 153 | class IndicatorState @ExperimentalPagerApi constructor( 154 | private val scope: CoroutineScope, 155 | val dotSettings: DotSettings 156 | ) { 157 | 158 | enum class DotState { 159 | IDLE, SCROLLING 160 | } 161 | 162 | var firstDotPosition: Offset? = null 163 | var animation = Animatable(initialValue = 0f) 164 | var animationSecond = Animatable(initialValue = 0f) 165 | 166 | var currentPosition = 0 167 | var targetPosition = 0 168 | var isScrollingBack = false 169 | var stateFirstDot by mutableStateOf(DotState.IDLE) 170 | var stateSecondDot by mutableStateOf(DotState.IDLE) 171 | 172 | fun startScrolling(targetValue: Int) { 173 | if (targetValue < dotSettings.size && currentPosition < dotSettings.size) { 174 | if (targetValue != currentPosition) { 175 | scope.launch { 176 | targetPosition = targetValue 177 | stateFirstDot = DotState.SCROLLING 178 | animation.snapTo(0f) 179 | animation.animateTo( 180 | targetValue = 1f, 181 | animationSpec = tween(durationMillis = 100, easing = LinearEasing) 182 | ) 183 | } 184 | } else if (!isScrollingBack) { 185 | isScrollingBack = true 186 | scope.launch { 187 | stateFirstDot = DotState.SCROLLING 188 | animation.snapTo(1f) 189 | animation.animateTo( 190 | targetValue = 0f, 191 | animationSpec = tween(durationMillis = 100, easing = LinearEasing) 192 | ) 193 | targetPosition = targetValue 194 | isScrollingBack = false 195 | } 196 | } 197 | } 198 | } 199 | 200 | fun finishScrolling() { 201 | stateSecondDot = DotState.IDLE 202 | if (targetPosition != currentPosition) { 203 | scope.launch { 204 | stateSecondDot = DotState.SCROLLING 205 | animationSecond.animateTo( 206 | targetValue = 1f, 207 | animationSpec = tween(durationMillis = 100, easing = LinearEasing) 208 | ) 209 | animationSecond.snapTo(0f) 210 | currentPosition = targetPosition 211 | stateSecondDot = DotState.IDLE 212 | } 213 | } 214 | } 215 | 216 | fun setFirstIndicatorPosition(center: Offset) { 217 | firstDotPosition = Offset( 218 | center.x - (((dotSettings.size - 1) * dotSettings.distanceBetweenDots) / 2), 219 | center.y 220 | ) 221 | } 222 | 223 | fun getOffset(target: Int) = Offset( 224 | firstDotPosition!!.x + dotSettings.distanceBetweenDots * target, 225 | firstDotPosition!!.y 226 | ) 227 | 228 | class DotSettings( 229 | var size: Int, 230 | var radius: Float, 231 | var distanceBetweenDots: Float = radius * 5, 232 | var color: Color = Color.White 233 | ) 234 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/RoundIndicators.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.Canvas 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.drawBehind 13 | import androidx.compose.ui.geometry.Offset 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.unit.dp 16 | import com.google.accompanist.pager.ExperimentalPagerApi 17 | import com.google.accompanist.pager.HorizontalPager 18 | import com.google.accompanist.pager.PagerState 19 | import com.google.accompanist.pager.rememberPagerState 20 | import com.represa.draw.data.desserts 21 | 22 | @ExperimentalPagerApi 23 | @Composable 24 | fun RoundIndicators() { 25 | val list = remember { desserts } 26 | val dotSettings = 27 | NewIndicatorState.NewDotSettings(size = list.size, radius = 12f) 28 | val pagerState = rememberPagerState(pageCount = list.size) 29 | val state = remember { NewIndicatorState(dotSettings) } 30 | 31 | Column( 32 | Modifier 33 | .fillMaxSize() 34 | .background(Color(0xFFFCFCFF)) 35 | ) { 36 | 37 | HorizontalPager( 38 | state = pagerState, 39 | itemSpacing = 10.dp, 40 | modifier = Modifier 41 | .fillMaxWidth() 42 | .padding(0.dp, 10.dp) 43 | ) { page -> 44 | // Our page content 45 | DessertCard(dessert = list[page]) 46 | } 47 | 48 | NewIndicators( 49 | state, 50 | pagerState, 51 | Modifier 52 | .fillMaxWidth() 53 | .padding(0.dp, 10.dp, 0.dp, 0.dp) 54 | ) 55 | 56 | } 57 | } 58 | 59 | @ExperimentalPagerApi 60 | @Composable 61 | fun NewIndicators(state: NewIndicatorState, pagerState: PagerState, modifier: Modifier) { 62 | drawIndicators(state = state, modifier = modifier) 63 | if (pagerState.currentPage != state.currentDot) { 64 | state.currentDot = pagerState.currentPage 65 | } 66 | } 67 | 68 | 69 | @Composable 70 | fun drawIndicators(state: NewIndicatorState, modifier: Modifier) { 71 | Box( 72 | modifier = modifier 73 | ) { 74 | Canvas(modifier = modifier) { 75 | state.setFirstIndicatorPosition(center) 76 | } 77 | for (i in 0 until state.dotSettings.size) { 78 | singleRoundIndicator(state = state, position = i) 79 | } 80 | } 81 | } 82 | 83 | @Composable 84 | fun singleRoundIndicator(state: NewIndicatorState, position: Int) { 85 | var dotSettings = state.dotSettings 86 | val alpha: Float by animateFloatAsState( 87 | if (state.currentDot == position) 1f else 0.5f, 88 | animationSpec = tween(durationMillis = 300, easing = LinearEasing) 89 | ) 90 | val size: Float by animateFloatAsState( 91 | if (state.currentDot == position) 1.5f else 0.9f, 92 | animationSpec = tween(durationMillis = 300, easing = LinearEasing) 93 | ) 94 | val strokeColor: Color by animateColorAsState(if (state.currentDot == position) dotSettings.strokeColorSelected else dotSettings.strokeColor) 95 | val fillColor: Color by animateColorAsState(if (state.currentDot == position) dotSettings.fillColorSelected else dotSettings.fillColor) 96 | Box(modifier = Modifier.drawBehind { 97 | drawCircle( 98 | color = strokeColor, 99 | radius = dotSettings.radius * 1.2f * size, 100 | center = Offset( 101 | state.firstDotPosition!!.x + dotSettings.distanceBetweenDots * position, 102 | state.firstDotPosition!!.y 103 | ), 104 | alpha = alpha 105 | ) 106 | drawCircle( 107 | color = fillColor, 108 | radius = dotSettings.radius * size, 109 | center = Offset( 110 | state.firstDotPosition!!.x + dotSettings.distanceBetweenDots * position, 111 | state.firstDotPosition!!.y 112 | ) 113 | ) 114 | }) { 115 | } 116 | } 117 | 118 | class NewIndicatorState @ExperimentalPagerApi constructor( 119 | val dotSettings: NewDotSettings 120 | ) { 121 | 122 | var firstDotPosition: Offset? = null 123 | 124 | var currentDot by mutableStateOf(0) 125 | 126 | fun setFirstIndicatorPosition(center: Offset) { 127 | firstDotPosition = Offset( 128 | center.x - (((dotSettings.size - 1) * dotSettings.distanceBetweenDots) / 2), 129 | center.y 130 | ) 131 | } 132 | 133 | class NewDotSettings( 134 | var size: Int, 135 | var radius: Float, 136 | var distanceBetweenDots: Float = radius * 5, 137 | var strokeColor: Color = Color.Gray, 138 | var fillColor: Color = Color.White, 139 | var strokeColorSelected: Color = Color.White, 140 | var fillColorSelected: Color = Color.Black 141 | ) 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/Splash.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.drawBehind 16 | import androidx.compose.ui.geometry.Offset 17 | import androidx.compose.ui.graphics.Color 18 | 19 | @Composable 20 | fun SplashScreen(onSucces: () -> Unit) { 21 | 22 | var sizeLine = remember{ Animatable(0f) } 23 | 24 | LaunchedEffect(sizeLine){ 25 | sizeLine.animateTo( 26 | targetValue = 1f, 27 | animationSpec = tween(durationMillis = 2000) 28 | ) 29 | onSucces.invoke() 30 | } 31 | 32 | Box(modifier = Modifier 33 | .fillMaxSize() 34 | .background(Color.Black) 35 | .drawBehind { 36 | drawLine( 37 | strokeWidth = 25f, 38 | color = Color.White, 39 | start = Offset(size.width / 2, 0f), 40 | end = Offset(size.width / 2, size.height * sizeLine.value) 41 | ) 42 | drawLine( 43 | strokeWidth = 25f, 44 | color = Color.White, 45 | start = Offset(size.width / 2 - 80, 0f), 46 | end = Offset(size.width / 2 - 80, size.height * sizeLine.value) 47 | ) 48 | drawLine( 49 | strokeWidth = 25f, 50 | color = Color.White, 51 | start = Offset(size.width / 2 + 80, 0f), 52 | end = Offset(size.width / 2 + 80, size.height * sizeLine.value) 53 | ) 54 | }) { 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/Stars.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material.Icon 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Star 9 | import androidx.compose.material.icons.filled.StarBorder 10 | import androidx.compose.material.icons.filled.StarOutline 11 | import androidx.compose.material.icons.filled.Stars 12 | import androidx.compose.runtime.* 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.alpha 15 | import androidx.compose.ui.draw.scale 16 | import androidx.compose.ui.unit.dp 17 | import kotlinx.coroutines.launch 18 | 19 | @Composable 20 | fun rate() { 21 | var starData = remember { StarData() } 22 | Row(Modifier.fillMaxSize()) { 23 | for (i in 1..5) { 24 | star(position = i, starData = starData) 25 | } 26 | } 27 | } 28 | 29 | @Composable 30 | fun star(position: Int, starData: StarData) { 31 | 32 | var scale = remember { Animatable(1f) } 33 | var coroutineScope = rememberCoroutineScope() 34 | 35 | var showFilled = starData.starList.contains(position) 36 | 37 | 38 | Box() { 39 | Icon(Icons.Default.StarOutline, contentDescription = "", modifier = Modifier 40 | .scale(scale.value) 41 | .clickable { 42 | starData.click(position) 43 | starData.click(position) 44 | coroutineScope.launch { 45 | scale.animateTo( 46 | targetValue = 1.7f, 47 | animationSpec = tween(durationMillis = 200, easing = LinearEasing) 48 | ) 49 | scale.animateTo( 50 | targetValue = 1f, 51 | animationSpec = tween(durationMillis = 200, easing = LinearEasing) 52 | ) 53 | } 54 | }) 55 | 56 | if (showFilled) { 57 | Icon(Icons.Default.Star, contentDescription = "", modifier = Modifier 58 | .scale(scale.value) 59 | .clickable { 60 | starData.click(position) 61 | coroutineScope.launch { 62 | scale.animateTo( 63 | targetValue = 1.7f, 64 | animationSpec = tween( 65 | durationMillis = 200, 66 | easing = LinearEasing 67 | ) 68 | ) 69 | scale.animateTo( 70 | targetValue = 1f, 71 | animationSpec = tween( 72 | durationMillis = 200, 73 | easing = LinearEasing 74 | ) 75 | ) 76 | } 77 | } 78 | ) 79 | } 80 | } 81 | } 82 | 83 | 84 | class StarData { 85 | var starList = mutableStateListOf() 86 | 87 | fun click(star: Int) { 88 | var list = mutableListOf() 89 | for (i in 1..star) { 90 | list.add(i) 91 | } 92 | starList.clear() 93 | starList.addAll(list) 94 | } 95 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.Color 9 | 10 | private val DarkColorPalette = darkColors( 11 | primary = Purple200, 12 | primaryVariant = Purple700, 13 | secondary = Teal200 14 | ) 15 | 16 | private val LightColorPalette = lightColors( 17 | primary = Purple500, 18 | primaryVariant = Purple700, 19 | secondary = Teal200, 20 | background = Color.White 21 | 22 | /* Other default colors to override 23 | background = Color.White, 24 | surface = Color.White, 25 | onPrimary = Color.White, 26 | onSecondary = Color.Black, 27 | onBackground = Color.Black, 28 | onSurface = Color.Black, 29 | */ 30 | ) 31 | 32 | @Composable 33 | fun DrawTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { 34 | val colors = LightColorPalette 35 | 36 | MaterialTheme( 37 | colors = colors, 38 | typography = Typography, 39 | shapes = Shapes, 40 | content = content 41 | ) 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/represa/draw/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.represa.draw.ui.theme 2 | 3 | import androidx.compose.material.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 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /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/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/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodrirepresa/ComposeAnimations/85ae7e02ac785d0aa50f4e21f02a727864652f9e/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /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 | Draw 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |