├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── ru │ │ │ └── ozh │ │ │ └── compose │ │ │ └── challenges │ │ │ ├── MainActivity.kt │ │ │ └── ui │ │ │ ├── Screen.kt │ │ │ ├── clock │ │ │ ├── Clock.kt │ │ │ ├── Dots.kt │ │ │ ├── Preview.kt │ │ │ └── Time.kt │ │ │ ├── grid │ │ │ ├── Cell.kt │ │ │ ├── Elements.kt │ │ │ ├── Icons.kt │ │ │ ├── WatchGridConfig.kt │ │ │ ├── WatchGridLayout.kt │ │ │ ├── WatchGridState.kt │ │ │ └── WatchGridStateImpl.kt │ │ │ ├── ktx │ │ │ ├── ModifierKtx.kt │ │ │ └── OffsetState.kt │ │ │ ├── magnet │ │ │ └── MagnetButtun.kt │ │ │ ├── switch │ │ │ ├── DayAndNightSwitch.kt │ │ │ ├── MoonIcon.kt │ │ │ ├── SunIcon.kt │ │ │ ├── SwitchConsts.kt │ │ │ └── SwitchMoon.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_app_store.xml │ │ ├── ic_apple_music.xml │ │ ├── ic_apple_tv.xml │ │ ├── ic_books.xml │ │ ├── ic_calculator.xml │ │ ├── ic_calendar.xml │ │ ├── ic_camera.xml │ │ ├── ic_clock.xml │ │ ├── ic_contacts.xml │ │ ├── ic_files.xml │ │ ├── ic_find_my.xml │ │ ├── ic_health.xml │ │ ├── ic_home.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_mail.xml │ │ ├── ic_maps.xml │ │ ├── ic_messages.xml │ │ ├── ic_notes.xml │ │ ├── ic_photos.xml │ │ ├── ic_podcasts.xml │ │ ├── ic_safari.xml │ │ ├── ic_settings.xml │ │ ├── ic_stocks.xml │ │ ├── ic_translate.xml │ │ ├── ic_wallet.xml │ │ └── ic_weather.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-v23 │ │ └── themes.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── ru │ └── ozh │ └── compose │ └── challenges │ └── ExampleUnitTest.kt ├── art ├── apple_grid_art.gif ├── clock.gif └── day_and_night_switch.gif ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── stability_config.conf /.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 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Compose-Challenges -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compose-Challenges 2 | 3 | Challenge | Art | 4 | ------------ |-----------------------------------| 5 | [Apple watch grid](/app/src/main/java/ru/ozh/compose/challenges/ui/grid/) | ![](art/apple_grid_art.gif) | 6 | [Day&Night Switch](/app/src/main/java/ru/ozh/compose/challenges/ui/switch/) | ![](art/day_and_night_switch.gif) | 7 | [Clock](/app/src/main/java/ru/ozh/compose/challenges/ui/clock/) | ![](art/clock.gif) | 8 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.compose) 5 | } 6 | 7 | android { 8 | namespace = "ru.ozh.compose.challenges" 9 | compileSdk = 35 10 | 11 | defaultConfig { 12 | applicationId = "ru.ozh.compose.challenges" 13 | minSdk = 22 14 | targetSdk = 35 15 | versionCode = 1 16 | versionName = "1.0" 17 | 18 | vectorDrawables { 19 | useSupportLibrary = true 20 | } 21 | } 22 | 23 | buildTypes { 24 | debug { 25 | isMinifyEnabled = false 26 | //proguardFiles getDefaultProguardFile ("proguard-android-optimize.txt"), "proguard-rules.pro" 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility = JavaVersion.VERSION_17 31 | targetCompatibility = JavaVersion.VERSION_17 32 | } 33 | kotlinOptions { 34 | jvmTarget = "17" 35 | } 36 | buildFeatures { 37 | compose = true 38 | } 39 | } 40 | 41 | composeCompiler { 42 | reportsDestination = layout.buildDirectory.dir("compose_compiler") 43 | metricsDestination = layout.buildDirectory.dir("compose_compiler") 44 | stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf") 45 | } 46 | 47 | dependencies { 48 | 49 | implementation (libs.androidx.material3) 50 | implementation (libs.androidx.material3.windows.size) 51 | 52 | implementation (libs.androidx.core.ktx) 53 | 54 | implementation(platform(libs.androidx.compose.bom)) 55 | implementation (libs.androidx.ui) 56 | implementation (libs.androidx.ui.tooling.preview) 57 | 58 | implementation (libs.androidx.lifecycle.runtime.ktx) 59 | implementation (libs.androidx.activity.compose) 60 | } -------------------------------------------------------------------------------- /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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.scale 13 | import kotlinx.coroutines.delay 14 | import ru.ozh.compose.challenges.ui.GridScreen 15 | import ru.ozh.compose.challenges.ui.clock.WaveClock 16 | import ru.ozh.compose.challenges.ui.clock.currentTime 17 | import ru.ozh.compose.challenges.ui.magnet.MagnetButtonScreen 18 | import ru.ozh.compose.challenges.ui.switch.SwitchDaN 19 | import ru.ozh.compose.challenges.ui.theme.ComposeChallengesTheme 20 | 21 | class MainActivity : ComponentActivity() { 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContent { 25 | ComposeChallengesTheme { 26 | // GridScreen() 27 | // SwitchScreen() 28 | // WaveClockScreen() 29 | MagnetButtonScreen() 30 | } 31 | } 32 | } 33 | } 34 | 35 | 36 | @Composable 37 | fun SwitchScreen() { 38 | Box( 39 | modifier = Modifier.fillMaxSize(), 40 | contentAlignment = Alignment.Center 41 | ) { 42 | var checked by remember { 43 | mutableStateOf(true) 44 | } 45 | SwitchDaN( 46 | checked = checked, 47 | onCheckedChange = { 48 | checked = it 49 | } 50 | ) 51 | } 52 | } 53 | 54 | @Composable 55 | fun WaveClockScreen() { 56 | var time by remember { mutableStateOf(currentTime()) } 57 | LaunchedEffect(Unit) { 58 | while (true) { 59 | time = currentTime() 60 | delay(1000) 61 | Log.d("WaveClockScreen", "$time ${time.hashCode()}") 62 | } 63 | } 64 | 65 | Box( 66 | modifier = Modifier.fillMaxSize().scale(2.5f), 67 | contentAlignment = Alignment.Center 68 | ) { 69 | WaveClock(time) 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/Screen.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.compose.foundation.BorderStroke 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.unit.dp 20 | import ru.ozh.compose.challenges.ui.grid.IconRounded 21 | import ru.ozh.compose.challenges.ui.grid.Icons 22 | import ru.ozh.compose.challenges.ui.grid.WatchGridLayout 23 | import ru.ozh.compose.challenges.ui.theme.ComposeChallengesTheme 24 | 25 | @Composable 26 | fun GridScreen() { 27 | 28 | var toast: Toast? = null 29 | val context: Context = LocalContext.current 30 | 31 | ComposeChallengesTheme { 32 | Box( 33 | modifier = Modifier 34 | .fillMaxSize(), 35 | contentAlignment = Alignment.Center 36 | ) { 37 | WatchGridLayout( 38 | modifier = Modifier 39 | .size(300.dp) 40 | .background( 41 | color = Color.Black, 42 | shape = RoundedCornerShape(size = 64.dp) 43 | ) 44 | .border( 45 | border = BorderStroke(width = 12.dp, color = Color.LightGray), 46 | shape = RoundedCornerShape(size = 64.dp) 47 | ) 48 | .clip( 49 | shape = RoundedCornerShape(size = 64.dp) 50 | ), 51 | rowItemsCount = 5, 52 | itemSize = 80.dp 53 | ) { 54 | Icons.appleIcons.forEach { (res, name) -> 55 | IconRounded(res = res, 56 | modifier = Modifier.clickable { 57 | toast?.cancel() 58 | toast = Toast.makeText( 59 | context, 60 | "You've clicked on $name", 61 | Toast.LENGTH_SHORT 62 | ) 63 | 64 | toast?.show() 65 | } 66 | ) 67 | } 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/clock/Clock.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalAnimationApi::class) 2 | 3 | package ru.ozh.compose.challenges.ui.clock 4 | 5 | import androidx.compose.animation.* 6 | import androidx.compose.animation.core.* 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.draw.shadow 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.unit.Dp 18 | import androidx.compose.ui.unit.TextUnit 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import kotlinx.coroutines.delay 22 | import kotlinx.coroutines.launch 23 | 24 | @Composable 25 | fun WaveClock( 26 | time: Time, 27 | plateSize: Dp = 40.dp, 28 | plateColor: Color = Color.White, 29 | plateElevation: Dp = 8.dp, 30 | plateRoundCorner: Dp = 3.dp, 31 | digitSize: TextUnit = 20.sp, 32 | digitColor: Color = Color.Black.copy(alpha = 0.6f) 33 | ) { 34 | Row( 35 | modifier = Modifier.wrapContentSize(), 36 | horizontalArrangement = Arrangement.Center, 37 | verticalAlignment = Alignment.CenterVertically 38 | ) { 39 | DigitPlate( 40 | value = time.hours, 41 | plateSize = plateSize, 42 | backgroundColor = plateColor, 43 | elevation = plateElevation, 44 | roundCorner = plateRoundCorner, 45 | textColor = digitColor, 46 | textSize = digitSize, 47 | animationDelay = 100 48 | ) 49 | DotsColumn(digitColor) 50 | DigitPlate( 51 | value = time.minutes, 52 | plateSize = plateSize, 53 | backgroundColor = plateColor, 54 | elevation = plateElevation, 55 | roundCorner = plateRoundCorner, 56 | textColor = digitColor, 57 | textSize = digitSize, 58 | animationDelay = 50 59 | ) 60 | DotsColumn(digitColor) 61 | DigitPlate( 62 | value = time.seconds, 63 | plateSize = plateSize, 64 | backgroundColor = plateColor, 65 | elevation = plateElevation, 66 | roundCorner = plateRoundCorner, 67 | textColor = digitColor, 68 | textSize = digitSize 69 | ) 70 | } 71 | } 72 | 73 | @Composable 74 | fun DigitPlate( 75 | value: Int, 76 | plateSize: Dp, 77 | roundCorner: Dp, 78 | backgroundColor: Color, 79 | textColor: Color, 80 | textSize: TextUnit, 81 | elevation: Dp = 0.dp, 82 | animationDelay: Long = 0, 83 | ) { 84 | val backGroundShape = RoundedCornerShape(size = roundCorner) 85 | WiggleBox( 86 | modifier = Modifier 87 | .shadow(elevation = elevation, shape = backGroundShape) 88 | .size(plateSize) 89 | .clip(shape = backGroundShape) 90 | .background(color = backgroundColor), 91 | key = value, 92 | shakeOffset = plateSize / 10, 93 | shakeDuration = 300, 94 | shakeDelay = animationDelay 95 | ) { 96 | DropDigitText( 97 | value = value, 98 | textColor = textColor, 99 | textSize = textSize, 100 | animationDelay = animationDelay 101 | ) 102 | } 103 | } 104 | 105 | @Composable 106 | fun DropDigitText( 107 | value: Int, 108 | textColor: Color, 109 | textSize: TextUnit, 110 | animationDelay: Long 111 | ) { 112 | SlideContentAnimation(targetState = value, delay = animationDelay) { targetCount -> 113 | Text( 114 | text = "%02d".format(targetCount), 115 | fontSize = textSize, 116 | color = textColor, 117 | ) 118 | } 119 | } 120 | 121 | @Composable 122 | fun WiggleBox( 123 | modifier: Modifier, 124 | key: Any, 125 | shakeOffset: Dp = 0.dp, 126 | shakeDuration: Int = 0, 127 | shakeDelay: Long = 0, 128 | content: @Composable BoxScope.() -> Unit 129 | ) { 130 | var ignoreFirstComposition by remember { mutableStateOf(true) } 131 | var targetOffset by remember { mutableStateOf(0.dp) } 132 | 133 | val animateOffset by animateDpAsState( 134 | targetValue = targetOffset, 135 | animationSpec = repeatable( 136 | iterations = 1, 137 | animation = tween( 138 | durationMillis = shakeDuration / 2, 139 | delayMillis = shakeDelay.toInt(), 140 | easing = FastOutSlowInEasing 141 | ), 142 | repeatMode = RepeatMode.Reverse 143 | ), 144 | finishedListener = { 145 | targetOffset = 0.dp 146 | } 147 | ) 148 | 149 | LaunchedEffect(key) { 150 | if (!ignoreFirstComposition) { 151 | targetOffset = shakeOffset 152 | } 153 | ignoreFirstComposition = false 154 | } 155 | 156 | Box( 157 | contentAlignment = Alignment.Center, 158 | modifier = Modifier 159 | .offset(y = animateOffset) 160 | .then(modifier), 161 | content = content 162 | ) 163 | } 164 | 165 | @Composable 166 | fun SlideContentAnimation( 167 | targetState: Int, 168 | delay: Long = 0, 169 | content: @Composable AnimatedVisibilityScope.(targetState: Int) -> Unit, 170 | ) { 171 | val scope = rememberCoroutineScope() 172 | 173 | AnimatedContent( 174 | targetState = targetState, 175 | transitionSpec = { 176 | scope.launch { delay(delay) } 177 | // Compare the incoming number with the previous number. 178 | if (targetState > initialState) { 179 | // If the target number is larger, it slides up and fades in 180 | // while the initial (smaller) number slides up and fades out. 181 | slideInVertically { height -> -height } with 182 | slideOutVertically { height -> height } 183 | } else { 184 | // If the target number is smaller, it slides down and fades in 185 | // while the initial number slides down and fades out. 186 | slideInVertically { height -> -height } with 187 | slideOutVertically { height -> height } 188 | }.using( 189 | // Disable clipping since the faded slide-in/out should 190 | // be displayed out of bounds. 191 | SizeTransform(clip = false) 192 | ) 193 | }, 194 | content = content 195 | ) 196 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/clock/Dots.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.clock 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.alpha 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun Dot( 15 | color: Color 16 | ) { 17 | Canvas(modifier = Modifier.size(2.dp)) { 18 | drawCircle( 19 | color = color, 20 | ) 21 | } 22 | } 23 | 24 | @Composable 25 | fun DotsColumn( 26 | dotColor: Color 27 | ) { 28 | 29 | val infiniteTransition = rememberInfiniteTransition() 30 | 31 | val alpha by infiniteTransition.animateFloat( 32 | initialValue = 1.0f, 33 | targetValue = 0.3f, 34 | animationSpec = infiniteRepeatable( 35 | animation = tween(durationMillis = 450), 36 | repeatMode = RepeatMode.Reverse 37 | ) 38 | ) 39 | 40 | Column( 41 | modifier = Modifier 42 | .width(8.dp) 43 | .alpha(alpha), 44 | verticalArrangement = Arrangement.Center, 45 | horizontalAlignment = Alignment.CenterHorizontally 46 | ) { 47 | Dot(dotColor) 48 | Spacer(modifier = Modifier.height(4.dp)) 49 | Dot(dotColor) 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/clock/Preview.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.clock 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.runtime.* 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import kotlinx.coroutines.delay 11 | 12 | @Preview 13 | @Composable 14 | fun WaveClockPreview() { 15 | var time by remember { mutableStateOf(currentTime()) } 16 | LaunchedEffect(Unit) { 17 | while (true) { 18 | time = currentTime() 19 | delay(1000) 20 | } 21 | } 22 | 23 | Box( 24 | modifier = Modifier.fillMaxSize(), 25 | contentAlignment = Alignment.Center 26 | ) { 27 | WaveClock(time) 28 | } 29 | } 30 | 31 | @Preview 32 | @Composable 33 | fun DotsColumnPreview() { 34 | DotsColumn(Color.Black) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/clock/Time.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.clock 2 | 3 | import java.util.* 4 | 5 | data class Time( 6 | val hours: Int, 7 | val minutes: Int, 8 | val seconds: Int 9 | ) 10 | 11 | fun currentTime(): Time { 12 | val calendar = Calendar.getInstance() 13 | return Time( 14 | hours = calendar.get(Calendar.HOUR_OF_DAY), 15 | minutes = calendar.get(Calendar.MINUTE), 16 | seconds = calendar.get(Calendar.SECOND), 17 | ) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/grid/Cell.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.grid 2 | 3 | data class Cell( 4 | val x: Int, 5 | val y: Int, 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/grid/Elements.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.grid 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.geometry.Offset 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.res.painterResource 16 | 17 | @Composable 18 | fun IconRounded( 19 | modifier: Modifier = Modifier, 20 | @DrawableRes res: Int 21 | ) { 22 | Box( 23 | modifier = Modifier.clip(CircleShape).then(modifier), 24 | contentAlignment = Alignment.Center 25 | ) { 26 | Image( 27 | modifier = Modifier.fillMaxSize(), 28 | painter = painterResource(id = res), 29 | contentDescription = "" 30 | ) 31 | } 32 | } 33 | 34 | @Composable 35 | fun CrossLine( 36 | color: Color = Color.White 37 | ) { 38 | Canvas( 39 | modifier = Modifier 40 | .fillMaxSize(), 41 | onDraw = { 42 | drawLine( 43 | start = Offset(this.size.width / 2, 0f), 44 | end = Offset(this.size.width / 2, this.size.height), 45 | color = color, 46 | strokeWidth = 4f 47 | ) 48 | 49 | drawLine( 50 | start = Offset(0f, this.size.height / 2), 51 | end = Offset(this.size.width, this.size.height / 2), 52 | color = color, 53 | strokeWidth = 4f 54 | ) 55 | } 56 | ) 57 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/grid/Icons.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.grid 2 | 3 | import ru.ozh.compose.challenges.R 4 | 5 | 6 | object Icons { 7 | val appleIcons = listOf( 8 | R.drawable.ic_app_store to "App store", 9 | R.drawable.ic_apple_music to "Apple music", 10 | R.drawable.ic_apple_tv to "Apple TV", 11 | R.drawable.ic_calculator to "Calculator", 12 | R.drawable.ic_books to "Books", 13 | R.drawable.ic_calendar to "Calendar", 14 | R.drawable.ic_camera to "Camera", 15 | R.drawable.ic_clock to "Clock", 16 | R.drawable.ic_contacts to "Contacts", 17 | R.drawable.ic_settings to "Settings", 18 | R.drawable.ic_files to "Files", 19 | R.drawable.ic_find_my to "Find_my", 20 | R.drawable.ic_health to "Health", 21 | R.drawable.ic_home to "Home", 22 | R.drawable.ic_mail to "Mail", 23 | R.drawable.ic_maps to "Maps", 24 | R.drawable.ic_messages to "Messages", 25 | R.drawable.ic_notes to "Notes", 26 | R.drawable.ic_photos to "Photos", 27 | R.drawable.ic_podcasts to "Podcasts", 28 | R.drawable.ic_safari to "Safari", 29 | R.drawable.ic_stocks to "Stocks", 30 | R.drawable.ic_translate to "Translate", 31 | R.drawable.ic_wallet to "Wallet", 32 | R.drawable.ic_weather to "Weather", 33 | ) 34 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/grid/WatchGridConfig.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.grid 2 | 3 | import androidx.compose.ui.unit.IntOffset 4 | 5 | class WatchGridConfig( 6 | val itemSizePx: Int = 0, 7 | val layoutHeightPx: Int = 0, 8 | val layoutWidthPx: Int = 0, 9 | val cells: List = emptyList() 10 | ) { 11 | 12 | val a = 3f * layoutWidthPx 13 | val b = 3f * layoutHeightPx 14 | val c = 20.0f 15 | 16 | val layoutCenter = IntOffset( 17 | x = layoutWidthPx / 2, 18 | y = layoutHeightPx / 2 19 | ) 20 | 21 | val halfItemSizePx = itemSizePx / 2 22 | 23 | val contentHeight = ((cells.maxByOrNull { it.y }?.y?.let { y -> y + 1 }) ?: 0).times(itemSizePx) 24 | val contentWidth = ((cells.maxByOrNull { it.x }?.x?.let { x -> x + 1 }) ?: 0).times(itemSizePx) 25 | 26 | val maxOffsetHorizontal = contentWidth - layoutWidthPx 27 | val maxOffsetVertical = contentHeight - layoutHeightPx 28 | 29 | val overScrollDragDistanceHorizontal = layoutWidthPx - itemSizePx 30 | val overScrollDragDistanceVertical = layoutHeightPx - itemSizePx 31 | 32 | val overScrollDistanceHorizontal = layoutWidthPx / 2 - halfItemSizePx 33 | val overScrollDistanceVertical = layoutHeightPx / 2 - halfItemSizePx 34 | 35 | val overScrollDragRangeVertical = 36 | (-maxOffsetVertical.toFloat() - overScrollDragDistanceVertical) 37 | .rangeTo(overScrollDragDistanceVertical.toFloat()) 38 | val overScrollDragRangeHorizontal = 39 | (-maxOffsetHorizontal.toFloat() - overScrollDragDistanceHorizontal) 40 | .rangeTo(overScrollDragDistanceHorizontal.toFloat()) 41 | 42 | val overScrollRangeVertical = 43 | (-maxOffsetVertical.toFloat() - overScrollDistanceVertical) 44 | .rangeTo(overScrollDistanceVertical.toFloat()) 45 | val overScrollRangeHorizontal = 46 | (-maxOffsetHorizontal.toFloat() - overScrollDistanceHorizontal) 47 | .rangeTo(overScrollDistanceHorizontal.toFloat()) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/grid/WatchGridLayout.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.grid 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.draw.clipToBounds 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.layout.Layout 10 | import androidx.compose.ui.platform.LocalDensity 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.Constraints 13 | import androidx.compose.ui.unit.Dp 14 | import androidx.compose.ui.unit.dp 15 | import ru.ozh.compose.challenges.ui.grid.Icons.appleIcons 16 | import ru.ozh.compose.challenges.ui.ktx.drag 17 | import ru.ozh.compose.challenges.ui.theme.ComposeChallengesTheme 18 | 19 | @Composable 20 | fun WatchGridLayout( 21 | modifier: Modifier = Modifier, 22 | rowItemsCount: Int, 23 | itemSize: Dp, 24 | state: WatchGridState = rememberWatchGridState(), 25 | content: @Composable () -> Unit, 26 | ) { 27 | 28 | check(rowItemsCount > 0) { "rowItemsCount must be positive" } 29 | check(itemSize > 0.dp) { "itemSize must be positive" } 30 | 31 | val itemSizePx = with(LocalDensity.current) { itemSize.roundToPx() } 32 | val itemConstraints = Constraints.fixed(width = itemSizePx, height = itemSizePx) 33 | 34 | Layout( 35 | modifier = modifier 36 | .drag(state) 37 | .clipToBounds(), 38 | content = content 39 | ) { measurables, layoutConstraints -> 40 | 41 | val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) } 42 | val cells = List(placeables.size) { index -> 43 | val x = index % rowItemsCount 44 | val y = (index - x) / rowItemsCount 45 | Cell(x, y) 46 | } 47 | 48 | state.config = WatchGridConfig( 49 | layoutWidthPx = layoutConstraints.maxWidth, 50 | layoutHeightPx = layoutConstraints.maxHeight, 51 | itemSizePx = itemSizePx, 52 | cells = cells 53 | ) 54 | 55 | layout(layoutConstraints.maxWidth, layoutConstraints.maxHeight) { 56 | placeables.forEachIndexed { index, placeable -> 57 | val position = state.getPositionFor(index) 58 | val scale = state.getScaleFor(position) 59 | placeable.placeWithLayer( 60 | position = position, 61 | layerBlock = { 62 | this.scaleX = scale 63 | this.scaleY = scale 64 | } 65 | ) 66 | } 67 | } 68 | } 69 | } 70 | 71 | @Preview(showBackground = true) 72 | @Composable 73 | fun WatchGridLayoutPreview() { 74 | ComposeChallengesTheme { 75 | WatchGridLayout( 76 | modifier = Modifier 77 | .size(300.dp) 78 | .background(Color.Black), 79 | rowItemsCount = 5, 80 | itemSize = 80.dp 81 | ) { 82 | appleIcons.forEach { (res, name) -> 83 | IconRounded( 84 | res = res 85 | ) 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/grid/WatchGridState.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.grid 2 | 3 | import androidx.compose.ui.unit.IntOffset 4 | import ru.ozh.compose.challenges.ui.ktx.OffsetState 5 | 6 | interface WatchGridState : OffsetState { 7 | 8 | var config: WatchGridConfig 9 | 10 | fun getPositionFor(index: Int): IntOffset 11 | fun getScaleFor(position: IntOffset): Float 12 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/grid/WatchGridStateImpl.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.grid 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.runtime.* 5 | import androidx.compose.runtime.saveable.Saver 6 | import androidx.compose.runtime.saveable.rememberSaveable 7 | import androidx.compose.ui.geometry.Offset 8 | import androidx.compose.ui.unit.IntOffset 9 | 10 | data class WatchGridStateImpl( 11 | private val initialOffset: Offset = Offset.Zero 12 | ) : WatchGridState { 13 | 14 | companion object { 15 | val Saver = Saver>( 16 | save = { 17 | val (x, y) = it.currentOffset 18 | listOf(x, y) 19 | }, 20 | restore = { 21 | WatchGridStateImpl(initialOffset = Offset(it[0], it[1])) 22 | } 23 | ) 24 | 25 | private const val CELL_MIN_SCALE = 0.5f 26 | private const val CELL_MAX_SCALE = 1.0f 27 | } 28 | 29 | private val decayAnimationSpec = SpringSpec( 30 | dampingRatio = Spring.DampingRatioLowBouncy, 31 | stiffness = Spring.StiffnessLow, 32 | ) 33 | 34 | override val animatable = Animatable( 35 | initialValue = initialOffset, 36 | typeConverter = Offset.VectorConverter 37 | ) 38 | 39 | override var config: WatchGridConfig = WatchGridConfig() 40 | 41 | override suspend fun snapTo(offset: Offset) { 42 | val x = offset.x.coerceIn(config.overScrollDragRangeHorizontal) 43 | val y = offset.y.coerceIn(config.overScrollDragRangeVertical) 44 | animatable.snapTo(Offset(x, y)) 45 | } 46 | 47 | override suspend fun animateTo(offset: Offset, velocity: Offset) { 48 | val x = offset.x.coerceIn(config.overScrollRangeHorizontal) 49 | val y = offset.y.coerceIn(config.overScrollRangeVertical) 50 | animatable.animateTo( 51 | initialVelocity = velocity, 52 | animationSpec = decayAnimationSpec, 53 | targetValue = Offset(x, y) 54 | ) 55 | } 56 | 57 | override suspend fun stop() { 58 | animatable.stop() 59 | } 60 | 61 | override fun getPositionFor(index: Int): IntOffset { 62 | val (offsetX, offsetY) = currentOffset 63 | val (cellX, cellY) = config.cells[index] 64 | val rowOffset = if (cellY % 2 != 0) { 65 | config.halfItemSizePx 66 | } else { 67 | 0 68 | } 69 | val x = (cellX * config.itemSizePx) + offsetX.toInt() + rowOffset 70 | val y = (cellY * config.itemSizePx) + offsetY.toInt() 71 | 72 | return IntOffset(x, y) 73 | } 74 | 75 | override fun getScaleFor(position: IntOffset): Float { 76 | val (centerX, centerY) = position.plus( 77 | IntOffset( 78 | config.halfItemSizePx, 79 | config.halfItemSizePx 80 | ) 81 | ) 82 | val offsetX = centerX - config.layoutCenter.x 83 | val offsetY = centerY - config.layoutCenter.y 84 | val x = (offsetX * offsetX) / (config.a * config.a) 85 | val y = (offsetY * offsetY) / (config.b * config.b) 86 | val z = (-config.c * (x + y) + 1.1f) 87 | .coerceIn( 88 | minimumValue = CELL_MIN_SCALE, 89 | maximumValue = CELL_MAX_SCALE 90 | ) 91 | return z 92 | } 93 | } 94 | 95 | @Composable 96 | fun rememberWatchGridState(): WatchGridState { 97 | return rememberSaveable(saver = WatchGridStateImpl.Saver) { 98 | WatchGridStateImpl() 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/ktx/ModifierKtx.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.ktx 2 | 3 | import androidx.compose.animation.core.VectorConverter 4 | import androidx.compose.animation.core.calculateTargetValue 5 | import androidx.compose.animation.splineBasedDecay 6 | import androidx.compose.foundation.gestures.* 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.geometry.Offset 9 | import androidx.compose.ui.input.pointer.* 10 | import androidx.compose.ui.input.pointer.util.VelocityTracker 11 | import androidx.compose.ui.input.pointer.util.addPointerInputChange 12 | import kotlinx.coroutines.coroutineScope 13 | import kotlinx.coroutines.launch 14 | 15 | fun Modifier.drag(state: OffsetState) = pointerInput(Unit) { 16 | val decay = splineBasedDecay(this) 17 | val tracker = VelocityTracker() 18 | coroutineScope { 19 | awaitEachGesture { 20 | val pointerId = awaitFirstDown(requireUnconsumed = false).id 21 | launch { 22 | state.stop() 23 | } 24 | tracker.resetTracking() 25 | var dragPointerInput: PointerInputChange? 26 | var overSlop = Offset.Zero 27 | do { 28 | dragPointerInput = awaitTouchSlopOrCancellation( 29 | pointerId 30 | ) { change, over -> 31 | if (change.positionChange() != Offset.Zero) change.consume() 32 | overSlop = over 33 | } 34 | } while (dragPointerInput != null && !dragPointerInput.isConsumed) 35 | 36 | dragPointerInput?.let { 37 | 38 | launch { 39 | state.snapTo(state.currentOffset.plus(overSlop)) 40 | } 41 | 42 | drag(dragPointerInput.id) { change -> 43 | val dragAmount = change.positionChange() 44 | launch { 45 | state.snapTo(state.currentOffset.plus(dragAmount)) 46 | } 47 | if (change.positionChange() != Offset.Zero) change.consume() 48 | tracker.addPointerInputChange(change) 49 | } 50 | } 51 | 52 | val (velX, velY) = tracker.calculateVelocity() 53 | val velocity = Offset(velX, velY) 54 | val targetOffset = decay.calculateTargetValue( 55 | typeConverter = Offset.VectorConverter, 56 | initialValue = state.currentOffset, 57 | initialVelocity = velocity 58 | ) 59 | launch { 60 | state.animateTo( 61 | offset = targetOffset, 62 | velocity = velocity, 63 | ) 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/ktx/OffsetState.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.ktx 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.AnimationVector2D 5 | import androidx.compose.ui.geometry.Offset 6 | 7 | /** 8 | * @see [drag] 9 | */ 10 | interface OffsetState { 11 | 12 | val currentOffset: Offset 13 | get() = animatable.value 14 | 15 | val animatable: Animatable 16 | 17 | suspend fun snapTo(offset: Offset) 18 | suspend fun animateTo(offset: Offset, velocity: Offset) 19 | suspend fun stop() 20 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/magnet/MagnetButtun.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.magnet 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.gestures.detectDragGestures 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.offset 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.geometry.Offset 19 | import androidx.compose.ui.geometry.Rect 20 | import androidx.compose.ui.geometry.RoundRect 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.input.pointer.pointerInput 23 | import androidx.compose.ui.platform.LocalDensity 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.Dp 26 | import androidx.compose.ui.unit.IntOffset 27 | import androidx.compose.ui.unit.dp 28 | 29 | @Composable 30 | fun MagnetButtonScreen() { 31 | Surface { 32 | MagnetButtonBox( 33 | modifier = Modifier 34 | .fillMaxSize() 35 | .background(color = Color.LightGray) 36 | ) 37 | } 38 | } 39 | 40 | @Preview 41 | @Composable 42 | fun MagnetButtonScreenPreview() { 43 | MagnetButtonScreen() 44 | } 45 | 46 | @Composable 47 | fun MagnetButtonBox( 48 | modifier: Modifier = Modifier, 49 | buttonSize: Dp = 72.dp, 50 | ) { 51 | BoxWithConstraints(modifier) { 52 | 53 | val boxWidth = with(LocalDensity.current) { maxWidth.toPx() } 54 | val boxHeight = with(LocalDensity.current) { maxHeight.toPx() } 55 | val buttonHalfSizePx = with(LocalDensity.current) { buttonSize.toPx() } 56 | 57 | val bounds = Rect( 58 | topLeft = Offset.Zero, 59 | bottomRight = Offset(boxWidth - buttonHalfSizePx, boxHeight - buttonHalfSizePx) 60 | ) 61 | 62 | var offsetX by remember { mutableStateOf(0) } 63 | var offsetY by remember { mutableStateOf(0) } 64 | Box( 65 | modifier = Modifier 66 | .offset { IntOffset(offsetX, offsetY) } 67 | .size(buttonSize) 68 | .background(color = Color.Blue, shape = RoundedCornerShape(20.dp)) 69 | .pointerInput(Unit) { 70 | detectDragGestures { change, dragAmount -> 71 | change.consume() 72 | val x = offsetX + dragAmount.x.toInt() 73 | val y = offsetY + dragAmount.y.toInt() 74 | if(bounds.contains(Offset(x.toFloat(), y.toFloat()))) { 75 | offsetX = x 76 | offsetY = y 77 | } 78 | } 79 | } 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/switch/DayAndNightSwitch.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.switch 2 | 3 | import android.view.animation.OvershootInterpolator 4 | import androidx.compose.animation.animateColorAsState 5 | import androidx.compose.animation.core.Animatable 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.indication 9 | import androidx.compose.foundation.interaction.InteractionSource 10 | import androidx.compose.foundation.interaction.MutableInteractionSource 11 | import androidx.compose.foundation.layout.* 12 | import androidx.compose.foundation.selection.toggleable 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material.ripple.rememberRipple 15 | import androidx.compose.runtime.* 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.scale 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.Shape 21 | import androidx.compose.ui.graphics.TransformOrigin 22 | import androidx.compose.ui.graphics.graphicsLayer 23 | import androidx.compose.ui.platform.LocalDensity 24 | import androidx.compose.ui.semantics.Role 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.IntOffset 27 | import androidx.compose.ui.unit.dp 28 | import kotlinx.coroutines.coroutineScope 29 | import kotlinx.coroutines.launch 30 | import ru.ozh.compose.challenges.ui.switch.SwitchConsts.AnimationDuration 31 | import ru.ozh.compose.challenges.ui.switch.SwitchConsts.Black 32 | import ru.ozh.compose.challenges.ui.switch.SwitchConsts.Orange 33 | import ru.ozh.compose.challenges.ui.switch.SwitchConsts.RippleRadius 34 | import ru.ozh.compose.challenges.ui.switch.SwitchConsts.SwitchHeight 35 | import ru.ozh.compose.challenges.ui.switch.SwitchConsts.SwitchWidth 36 | import ru.ozh.compose.challenges.ui.switch.SwitchConsts.ThumbDiameter 37 | import ru.ozh.compose.challenges.ui.switch.SwitchConsts.ThumbPathLength 38 | import ru.ozh.compose.challenges.ui.switch.SwitchConsts.ThumbStartOffset 39 | import kotlin.math.roundToInt 40 | 41 | 42 | @Composable 43 | fun SwitchDaN( 44 | checked: Boolean, 45 | onCheckedChange: ((Boolean) -> Unit)?, 46 | modifier: Modifier = Modifier, 47 | enabled: Boolean = true, 48 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 49 | ) { 50 | 51 | val minBound = with(LocalDensity.current) { ThumbStartOffset.toPx() } 52 | val maxBound = with(LocalDensity.current) { ThumbPathLength.toPx() } 53 | val valueToOffset = remember<(Boolean) -> Float>(minBound, maxBound) { 54 | { value -> if (value) maxBound else minBound } 55 | } 56 | 57 | val targetValue = valueToOffset(checked) 58 | val offset = remember { Animatable(targetValue) } 59 | val scaleX = remember { Animatable(1f) } 60 | val scaleY = remember { Animatable(1f) } 61 | 62 | val scope = rememberCoroutineScope() 63 | 64 | DisposableEffect(checked) { 65 | if (offset.targetValue != targetValue) { 66 | scope.launch { 67 | launch { 68 | offset.animateTo( 69 | targetValue = targetValue, 70 | animationSpec = tween( 71 | durationMillis = AnimationDuration, 72 | easing = OvershootInterpolator(1.5f).toEasing() 73 | ) 74 | ) 75 | } 76 | 77 | launch { 78 | scaleY.animateTo( 79 | targetValue = 0.8f, 80 | animationSpec = tween( 81 | durationMillis = AnimationDuration / 2, 82 | ) 83 | ) 84 | 85 | coroutineScope { 86 | launch { 87 | scaleX.animateTo( 88 | targetValue = 0.9f, 89 | animationSpec = tween( 90 | durationMillis = AnimationDuration / 4, 91 | ) 92 | ) 93 | 94 | scaleX.animateTo( 95 | targetValue = 1f, 96 | animationSpec = tween( 97 | durationMillis = AnimationDuration / 2, 98 | ) 99 | ) 100 | } 101 | launch { 102 | scaleY.animateTo( 103 | targetValue = 1.1f, 104 | animationSpec = tween( 105 | durationMillis = AnimationDuration / 4, 106 | ) 107 | ) 108 | 109 | scaleY.animateTo( 110 | targetValue = 1f, 111 | animationSpec = tween( 112 | durationMillis = AnimationDuration / 2, 113 | ) 114 | ) 115 | } 116 | } 117 | } 118 | } 119 | } 120 | onDispose { } 121 | } 122 | 123 | val toggleableModifier = if (onCheckedChange != null) { 124 | Modifier.toggleable( 125 | value = checked, 126 | onValueChange = onCheckedChange, 127 | enabled = enabled, 128 | role = Role.Switch, 129 | interactionSource = interactionSource, 130 | indication = null 131 | ) 132 | } else { 133 | Modifier 134 | } 135 | 136 | Box( 137 | modifier 138 | .then(toggleableModifier) 139 | .wrapContentSize(Alignment.Center) 140 | .requiredSize(SwitchWidth, SwitchHeight) 141 | ) { 142 | SwitchImpl( 143 | scaleX = scaleX.asState(), 144 | scaleY = scaleY.asState(), 145 | checked = checked, 146 | thumbValue = offset.asState(), 147 | interactionSource = interactionSource, 148 | thumbShape = RoundedCornerShape(50), 149 | ) 150 | } 151 | } 152 | 153 | @Composable 154 | private fun BoxScope.SwitchImpl( 155 | checked: Boolean, 156 | thumbValue: State, 157 | scaleX: State, 158 | scaleY: State, 159 | interactionSource: InteractionSource, 160 | thumbShape: Shape, 161 | ) { 162 | val trackColor by animateColorAsState(if (checked) Orange else Black) 163 | 164 | val modifier = Modifier 165 | .align(Alignment.Center) 166 | .width(SwitchWidth) 167 | .height(SwitchHeight) 168 | .background(trackColor, RoundedCornerShape(50)) 169 | 170 | Box(modifier) { 171 | 172 | if (checked) { 173 | SunIcon( 174 | modifier = Modifier 175 | .offset(x = 2.dp) 176 | .requiredSize(24.dp) 177 | .align(Alignment.CenterStart) 178 | .scale(0.8f), 179 | color = Color.White 180 | ) 181 | } else { 182 | MoonIcon( 183 | modifier = Modifier 184 | .offset(x = (-2).dp) 185 | .requiredSize(24.dp) 186 | .align(Alignment.CenterEnd) 187 | .scale(0.8f), 188 | color = Color.White 189 | ) 190 | } 191 | 192 | Box( 193 | modifier = Modifier 194 | .align(Alignment.CenterStart) 195 | .graphicsLayer { 196 | this.scaleX = scaleX.value 197 | this.scaleY = scaleY.value 198 | this.transformOrigin = if (checked) { 199 | TransformOrigin(3f, 0.5f) 200 | } else { 201 | TransformOrigin(0f, 0.5f) 202 | } 203 | } 204 | .offset { IntOffset(thumbValue.value.roundToInt(), 0) } 205 | // .indication( 206 | // interactionSource = interactionSource, 207 | //indication = rememberRipple(bounded = false, RippleRadius) 208 | // ) 209 | .requiredSize(ThumbDiameter) 210 | .background(Color.White, thumbShape), 211 | contentAlignment = Alignment.Center 212 | ) { 213 | 214 | } 215 | } 216 | } 217 | 218 | @Preview( 219 | widthDp = 300, 220 | heightDp = 300 221 | ) 222 | @Composable 223 | fun SwitchDaNPreview() { 224 | Box( 225 | modifier = Modifier.fillMaxSize(), 226 | contentAlignment = Alignment.Center 227 | ) { 228 | var checked by remember { 229 | mutableStateOf(true) 230 | } 231 | SwitchDaN( 232 | checked = checked, 233 | onCheckedChange = { 234 | checked = it 235 | } 236 | ) 237 | } 238 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/switch/MoonIcon.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.switch 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.TweenSpec 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.graphics.Path 13 | import androidx.compose.ui.graphics.drawscope.Stroke 14 | import androidx.compose.ui.graphics.drawscope.rotate 15 | import androidx.compose.ui.graphics.drawscope.scale 16 | import androidx.compose.ui.platform.LocalDensity 17 | import androidx.compose.ui.unit.dp 18 | import kotlinx.coroutines.launch 19 | 20 | @Composable 21 | fun MoonIcon( 22 | modifier: Modifier, 23 | color: Color 24 | ) { 25 | 26 | BoxWithConstraints(modifier) { 27 | val width = this.maxWidth 28 | val height = this.maxHeight 29 | val sizePx = with(LocalDensity.current) { width.toPx() } 30 | 31 | require(width == height) { 32 | error("width and height must be equals, but width = $width & height = $height") 33 | } 34 | 35 | val scaleAnimation = remember { Animatable(0.8f) } 36 | val rotateAnimation = remember { Animatable(180f) } 37 | val moonStrokeWidth = with(LocalDensity.current) { 1.dp.toPx() } 38 | val path = Path() 39 | 40 | LaunchedEffect(Unit) { 41 | launch { 42 | rotateAnimation.animateTo( 43 | 0f, 44 | animationSpec = TweenSpec( 45 | durationMillis = 250 46 | ) 47 | ) 48 | } 49 | 50 | launch { 51 | scaleAnimation.animateTo( 52 | 1f, 53 | animationSpec = TweenSpec( 54 | durationMillis = 250 55 | ) 56 | ) 57 | } 58 | } 59 | 60 | Canvas(modifier = modifier) { 61 | path.reset() 62 | path.moveTo(x = sizePx * 0.7f, y = 0f) 63 | path.cubicTo( 64 | x1 = 0f, 65 | y1 = 0f, 66 | x2 = 0f, 67 | y2 = sizePx, 68 | x3 = sizePx * 0.7f, 69 | y3 = sizePx, 70 | ) 71 | 72 | path.moveTo(x = sizePx * 0.7f, y = 0f) 73 | path.cubicTo( 74 | x1 = sizePx * 0.3f, 75 | y1 = sizePx * 0.2f, 76 | x2 = sizePx * 0.3f, 77 | y2 = sizePx * 0.8f, 78 | x3 = sizePx * 0.7f, 79 | y3 = sizePx, 80 | ) 81 | 82 | scale(scale = scaleAnimation.value) { 83 | rotate(rotateAnimation.value - 15f) { 84 | drawPath(path, color, style = Stroke(width = moonStrokeWidth)) 85 | } 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/switch/SunIcon.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.switch 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.TweenSpec 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.BoxWithConstraints 9 | import androidx.compose.foundation.layout.requiredSize 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.StrokeCap 18 | import androidx.compose.ui.graphics.drawscope.Stroke 19 | import androidx.compose.ui.graphics.drawscope.rotate 20 | import androidx.compose.ui.platform.LocalDensity 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import androidx.compose.ui.unit.dp 23 | import kotlinx.coroutines.delay 24 | import kotlinx.coroutines.launch 25 | 26 | @Composable 27 | fun SunIcon( 28 | modifier: Modifier, 29 | color: Color 30 | ) { 31 | 32 | BoxWithConstraints(modifier) { 33 | val width = this.maxWidth 34 | val height = this.maxHeight 35 | 36 | require(width == height) { 37 | error("width and height must be equals, but width = $width & height = $height") 38 | } 39 | 40 | val LightCount = 8 41 | val LightRotateAngle = 45f 42 | val sizePx = with(LocalDensity.current) { width.toPx() } 43 | val circleRadius = with(LocalDensity.current) { 4.dp.toPx() } 44 | val strokeWidth = with(LocalDensity.current) { 2.dp.toPx() } 45 | 46 | val circleAnimation = remember { Animatable(0f) } 47 | val lightsAlphaAnimation = remember { Array(LightCount) { Animatable(0f) } } 48 | val lightsOffsetAnimation = remember { Array(LightCount) { Animatable(0.75f) } } 49 | 50 | LaunchedEffect(Unit) { 51 | circleAnimation.animateTo( 52 | 1f, 53 | animationSpec = TweenSpec( 54 | durationMillis = 75 55 | ) 56 | ) 57 | 58 | lightsAlphaAnimation.zip(lightsOffsetAnimation) { alpha, offset -> alpha to offset } 59 | .withIndex() 60 | .forEach { (index, value) -> 61 | val (alpha, offset) = value 62 | delay(index * 5L) 63 | launch { 64 | alpha.animateTo( 65 | 1f, 66 | animationSpec = TweenSpec( 67 | durationMillis = 10 68 | ) 69 | ) 70 | } 71 | launch { 72 | offset.animateTo( 73 | 1f, 74 | animationSpec = TweenSpec( 75 | durationMillis = 10 76 | ) 77 | ) 78 | } 79 | } 80 | } 81 | 82 | Canvas(modifier = modifier) { 83 | 84 | drawCircle( 85 | color = color, 86 | radius = circleRadius * circleAnimation.value, 87 | style = Stroke(width = strokeWidth) 88 | ) 89 | 90 | repeat(LightCount) { index -> 91 | rotate(degrees = -90 + (index * LightRotateAngle)) { 92 | val lightStartX = sizePx * 0.8f 93 | val lightEndX = sizePx * if (index % 2 == 0) 1f else 0.95f 94 | 95 | val lightY = sizePx / 2 96 | 97 | val lightStart = Offset( 98 | x = lightStartX * lightsOffsetAnimation[index].value, 99 | y = lightY 100 | ) 101 | val lightEnd = Offset( 102 | x = lightEndX * lightsOffsetAnimation[index].value, 103 | y = lightY 104 | ) 105 | 106 | drawLine( 107 | color = color.copy(alpha = lightsAlphaAnimation[index].value), 108 | start = lightStart, 109 | end = lightEnd, 110 | strokeWidth = strokeWidth * lightsOffsetAnimation[index].value, 111 | cap = StrokeCap.Round 112 | ) 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | @Preview(widthDp = 32, heightDp = 32, showBackground = false) 120 | @Composable 121 | fun SunIconPreview() { 122 | Box( 123 | modifier = Modifier 124 | .requiredSize(24.dp) 125 | .background(color = Color.Blue) 126 | ) { 127 | SunIcon( 128 | modifier = Modifier 129 | .requiredSize(24.dp) 130 | .align(Alignment.CenterStart), 131 | color = Color.White, 132 | ) 133 | } 134 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/switch/SwitchConsts.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.switch 2 | 3 | import android.animation.TimeInterpolator 4 | import androidx.compose.animation.core.* 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.unit.dp 7 | 8 | internal object SwitchConsts { 9 | 10 | val AnimationDuration = 250 11 | val RippleRadius = 20.0.dp 12 | val SwitchWidth = 60.0.dp 13 | val SwitchHeight = 32.0.dp 14 | val ThumbDiameter = 24.0.dp 15 | val ThumbStartOffset = 4.dp 16 | val ThumbPadding = (SwitchHeight - ThumbDiameter) / 2 17 | val ThumbPathLength = (SwitchWidth - ThumbDiameter) - ThumbPadding 18 | 19 | val Black = Color(red = 53, green = 53, blue = 53) 20 | val Orange = Color(red = 243, green = 109, blue = 24) 21 | } 22 | 23 | 24 | fun TimeInterpolator.toEasing() = Easing { x -> getInterpolation(x) } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/switch/SwitchMoon.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.switch 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.Switch 8 | import androidx.compose.material3.SwitchDefaults 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.drawscope.translate 18 | import androidx.compose.ui.semantics.contentDescription 19 | import androidx.compose.ui.semantics.semantics 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | 23 | @Composable 24 | fun SwitchMoon() { 25 | var isChecked by remember { mutableStateOf(true) } 26 | val colors = SwitchDefaults.colors().copy( 27 | uncheckedBorderColor = Color.Transparent, 28 | checkedThumbColor = Color.Transparent, 29 | checkedTrackColor = Color(0XFF261439), 30 | uncheckedTrackColor = Color(0xFFFFECB3), 31 | ) 32 | Switch( 33 | modifier = Modifier.semantics { 34 | contentDescription = "SwitchMoon" 35 | }, 36 | checked = isChecked, 37 | onCheckedChange = { isChecked = it }, 38 | colors = colors, 39 | thumbContent = { MoonThumb(isChecked) } 40 | ) 41 | } 42 | 43 | @Composable 44 | @Preview 45 | fun SwitchMoonPreview() { 46 | SwitchMoon() 47 | } 48 | 49 | @Composable 50 | fun MoonThumb(isChecked: Boolean) { 51 | val translateFactor by animateFloatAsState( 52 | targetValue = if (isChecked) 0.45f else 1f, 53 | label = "translateFactor", 54 | ) 55 | Canvas( 56 | modifier = Modifier 57 | .size(24.dp) 58 | .clip(RoundedCornerShape(40)), 59 | ) { 60 | val diameter = size.maxDimension 61 | val radius = diameter / 2 62 | drawCircle( 63 | color = Color(0XFFFCC008), 64 | radius = radius, 65 | ) 66 | 67 | translate(left = -diameter * translateFactor) { 68 | drawCircle( 69 | color = Color(0XFF261439), 70 | radius = radius 71 | ) 72 | } 73 | } 74 | } 75 | 76 | @Composable 77 | @Preview 78 | fun MoonThumbPreview() { 79 | val isChecked by remember { mutableStateOf(false) } 80 | MoonThumb(isChecked) 81 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.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/ru/ozh/compose/challenges/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.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/ru/ozh/compose/challenges/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.SideEffect 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.toArgb 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.platform.LocalView 13 | import androidx.core.view.ViewCompat 14 | 15 | private val DarkColorScheme = darkColorScheme( 16 | primary = Purple80, 17 | secondary = PurpleGrey80, 18 | tertiary = Pink80 19 | ) 20 | 21 | private val LightColorScheme = lightColorScheme( 22 | primary = Purple40, 23 | secondary = PurpleGrey40, 24 | tertiary = Pink40, 25 | background = Color(0xFFFFFBFE), 26 | surface = Color(0xFFFFFBFE), 27 | onPrimary = Color.White, 28 | onSecondary = Color.White, 29 | onTertiary = Color.White, 30 | onBackground = Color(0xFF1C1B1F), 31 | onSurface = Color(0xFF1C1B1F), 32 | ) 33 | 34 | @Composable 35 | fun ComposeChallengesTheme( 36 | darkTheme: Boolean = isSystemInDarkTheme(), 37 | // Dynamic color is available on Android 12+ 38 | dynamicColor: Boolean = true, 39 | content: @Composable () -> Unit 40 | ) { 41 | val colorScheme = when { 42 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 43 | val context = LocalContext.current 44 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 45 | } 46 | darkTheme -> DarkColorScheme 47 | else -> LightColorScheme 48 | } 49 | val view = LocalView.current 50 | if (!view.isInEditMode) { 51 | SideEffect { 52 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 53 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 54 | } 55 | } 56 | 57 | MaterialTheme( 58 | colorScheme = colorScheme, 59 | typography = Typography, 60 | content = content 61 | ) 62 | } -------------------------------------------------------------------------------- /app/src/main/java/ru/ozh/compose/challenges/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges.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_app_store.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 17 | 20 | 23 | 24 | 25 | 26 | 30 | 34 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_apple_music.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 17 | 20 | 23 | 24 | 25 | 26 | 30 | 33 | 34 | 39 | 42 | 45 | 46 | 47 | 48 | 51 | 52 | 57 | 60 | 63 | 66 | 67 | 68 | 69 | 72 | 73 | 79 | 82 | 85 | 86 | 87 | 88 | 92 | 93 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_apple_tv.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 28 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_books.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_calculator.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_calendar.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_camera.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 28 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clock.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 22 | 26 | 30 | 34 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_contacts.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 15 | 18 | 21 | 22 | 23 | 29 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 46 | 49 | 52 | 53 | 54 | 55 | 58 | 62 | 65 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_files.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 15 | 19 | 23 | 26 | 27 | 33 | 36 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_find_my.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 27 | 30 | 33 | 36 | 39 | 40 | 41 | 47 | 50 | 53 | 56 | 59 | 62 | 65 | 66 | 67 | 68 | 71 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_health.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | 20 | 23 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /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/ic_mail.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 17 | 20 | 23 | 24 | 25 | 26 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_maps.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | 12 | 16 | 20 | 24 | 30 | 34 | 40 | 44 | 48 | 52 | 56 | 60 | 68 | 72 | 76 | 81 | 87 | 93 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_messages.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 17 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notes.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | 15 | 16 | 17 | 23 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 48 | 49 | 52 | 55 | 58 | 61 | 64 | 67 | 70 | 73 | 76 | 79 | 82 | 85 | 88 | 91 | 94 | 97 | 100 | 103 | 104 | 105 | 111 | 114 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_photos.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 22 | 26 | 30 | 34 | 38 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_podcasts.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 17 | 20 | 23 | 24 | 25 | 26 | 29 | 32 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_safari.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 40 | 46 | 52 | 58 | 64 | 70 | 76 | 82 | 88 | 94 | 100 | 106 | 112 | 118 | 124 | 130 | 136 | 142 | 148 | 154 | 160 | 166 | 172 | 178 | 184 | 190 | 196 | 202 | 208 | 214 | 220 | 226 | 232 | 238 | 244 | 250 | 254 | 258 | 259 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stocks.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | 15 | 18 | 19 | 25 | 28 | 31 | 32 | 33 | 34 | 37 | 42 | 47 | 52 | 55 | 60 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_translate.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_wallet.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_weather.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /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/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-v23/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /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-Challenges 3 | MainActivity3 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/test/java/ru/ozh/compose/challenges/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package ru.ozh.compose.challenges 2 | 3 | -------------------------------------------------------------------------------- /art/apple_grid_art.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/art/apple_grid_art.gif -------------------------------------------------------------------------------- /art/clock.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/art/clock.gif -------------------------------------------------------------------------------- /art/day_and_night_switch.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/art/day_and_night_switch.gif -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | plugins { 10 | alias(libs.plugins.android.application) apply false 11 | alias(libs.plugins.kotlin.android) apply false 12 | alias(libs.plugins.kotlin.compose) apply false 13 | } 14 | 15 | //task clean(type: Delete) { 16 | // delete rootProject.layout.buildDirectory 17 | //} -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | activityCompose = "1.9.3" 3 | agp = "8.6.1" 4 | composeBom = "2024.10.00" 5 | coreKtx = "1.13.1" 6 | kotlin = "2.0.20" 7 | lifecycleRuntimeKtx = "2.8.6" 8 | material3 = "1.3.0" 9 | 10 | [libraries] 11 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } 12 | androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } 13 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } 14 | androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } 15 | androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } 16 | androidx-material3-windows-size = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material3" } 17 | androidx-ui = { module = "androidx.compose.ui:ui" } 18 | androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } 19 | 20 | [plugins] 21 | android-application = { id = "com.android.application", version.ref = "agp" } 22 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 23 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Aug 01 12:18:59 MSK 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "Compose-Challenges" 16 | include ':app' 17 | -------------------------------------------------------------------------------- /stability_config.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozh-dev/Compose-Challenges/64575bcbc58b9c721ac8aac0cb929ed4358c24c7/stability_config.conf --------------------------------------------------------------------------------