├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── binissa │ │ └── sharedelementtransitionexample │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── binissa │ │ │ └── sharedelementtransitionexample │ │ │ ├── FoodOrderingApp.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MyApp.kt │ │ │ ├── screens │ │ │ └── food_ordering_screen │ │ │ │ ├── FoodOrderingScreen.kt │ │ │ │ ├── FoodOrderingViewModel.kt │ │ │ │ ├── FoodScreenState.kt │ │ │ │ └── components │ │ │ │ ├── FoodItemCard.kt │ │ │ │ ├── FoodItemDetailView.kt │ │ │ │ ├── FoodItemListItem.kt │ │ │ │ ├── NutritionInfoItem.kt │ │ │ │ ├── SelectionButton.kt │ │ │ │ └── TopAppBarForMainView.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Grid2x2.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── bakery_cake_dessert.xml │ │ ├── beverage_coffee_cup.xml │ │ ├── bowl_food.xml │ │ ├── burrito.xml │ │ ├── doughnut.xml │ │ ├── french_fries.xml │ │ ├── hamburger.xml │ │ ├── hotdog.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── pizza.xml │ │ ├── sandwich.xml │ │ ├── soup.xml │ │ └── sushi.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 │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── binissa │ └── sharedelementtransitionexample │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Shared Element Transition Example -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 57 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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 | alias(libs.plugins.ksp) 6 | alias(libs.plugins.hilt) 7 | 8 | } 9 | 10 | android { 11 | namespace = "com.binissa.sharedelementtransitionexample" 12 | compileSdk = 35 13 | 14 | defaultConfig { 15 | applicationId = "com.binissa.sharedelementtransitionexample" 16 | minSdk = 24 17 | targetSdk = 35 18 | versionCode = 1 19 | versionName = "1.0" 20 | 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | 24 | buildTypes { 25 | release { 26 | isMinifyEnabled = false 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_11 35 | targetCompatibility = JavaVersion.VERSION_11 36 | } 37 | kotlinOptions { 38 | jvmTarget = "11" 39 | } 40 | buildFeatures { 41 | compose = true 42 | } 43 | } 44 | 45 | dependencies { 46 | 47 | implementation(libs.androidx.core.ktx) 48 | implementation(libs.androidx.lifecycle.runtime.ktx) 49 | implementation(libs.androidx.activity.compose) 50 | implementation(platform(libs.androidx.compose.bom)) 51 | implementation(libs.androidx.ui) 52 | implementation(libs.androidx.ui.graphics) 53 | implementation(libs.androidx.ui.tooling.preview) 54 | implementation(libs.androidx.material3) 55 | testImplementation(libs.junit) 56 | androidTestImplementation(libs.androidx.junit) 57 | androidTestImplementation(libs.androidx.espresso.core) 58 | androidTestImplementation(platform(libs.androidx.compose.bom)) 59 | androidTestImplementation(libs.androidx.ui.test.junit4) 60 | debugImplementation(libs.androidx.ui.tooling) 61 | debugImplementation(libs.androidx.ui.test.manifest) 62 | 63 | implementation(libs.haze) 64 | 65 | implementation(libs.hilt.android) 66 | ksp(libs.hilt.compiler) 67 | implementation(libs.androidx.hilt.navigation.compose) 68 | 69 | 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/binissa/sharedelementtransitionexample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.binissa.sharedelementtransitionexample", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/FoodOrderingApp.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNREACHABLE_CODE") 2 | 3 | package com.binissa.sharedelementtransitionexample.screens.home 4 | 5 | import com.binissa.sharedelementtransitionexample.R 6 | 7 | // Enhanced FoodItem data class with more details 8 | data class FoodItem( 9 | val id: Int, 10 | val name: String, 11 | val price: String, 12 | val rating: Float, 13 | val imageRes: Int, 14 | val description: String, 15 | val ingredients: List, 16 | val calories: Int, 17 | val preparationTime: Int, // in minutes 18 | val isVegetarian: Boolean, 19 | val reviews: List, 20 | val isSelected: Boolean = false, 21 | val isInCart: Boolean = false, 22 | val quantity: Int = 0 23 | ) 24 | 25 | // Review data class for food items 26 | data class ReviewItem( 27 | val reviewerName: String, 28 | val rating: Float, 29 | val comment: String, 30 | val date: String 31 | ) 32 | 33 | // Expanded sample list of food items with rich data 34 | val sampleFoodItems = listOf( 35 | FoodItem( 36 | id = 0, 37 | name = "French Fries", 38 | price = "$7.25", 39 | rating = 4.5f, 40 | imageRes = R.drawable.french_fries, 41 | description = "Crispy golden fries made from premium russet potatoes, perfectly seasoned with sea salt.", 42 | ingredients = listOf("Russet potatoes", "Vegetable oil", "Sea salt"), 43 | calories = 312, 44 | preparationTime = 15, 45 | isVegetarian = true, 46 | reviews = listOf( 47 | ReviewItem( 48 | "Emma Wilson", 49 | 4.5f, 50 | "These fries are perfectly crispy! Great portion size too.", 51 | "Feb 12, 2025" 52 | ), 53 | ReviewItem( 54 | "James Smith", 55 | 4.0f, 56 | "Good flavor but could be a bit crispier. Still very tasty.", 57 | "Jan 30, 2025" 58 | ) 59 | ) 60 | ), 61 | FoodItem( 62 | id = 1, 63 | name = "Doughnut", 64 | price = "$10.00", 65 | rating = 3.8f, 66 | imageRes = R.drawable.doughnut, 67 | description = "Soft and fluffy glazed doughnut with a melt-in-your-mouth texture.", 68 | ingredients = listOf("Flour", "Sugar", "Yeast", "Eggs", "Milk", "Butter", "Vanilla glaze"), 69 | calories = 289, 70 | preparationTime = 30, 71 | isVegetarian = true, 72 | reviews = listOf( 73 | ReviewItem( 74 | "Sophia Chen", 75 | 3.5f, 76 | "Good sweetness but a bit too dense for my taste.", 77 | "Mar 5, 2025" 78 | ), 79 | ReviewItem( 80 | "Noah Johnson", 81 | 4.0f, 82 | "Classic glazed goodness! My kids love them.", 83 | "Feb 22, 2025" 84 | ) 85 | ) 86 | ), 87 | FoodItem( 88 | id = 2, 89 | name = "Hamburger", 90 | price = "$12.50", 91 | rating = 4.7f, 92 | imageRes = R.drawable.hamburger, 93 | description = "Premium beef patty with fresh lettuce, tomato, pickles, and our signature sauce on a toasted brioche bun.", 94 | ingredients = listOf( 95 | "Beef patty", 96 | "Brioche bun", 97 | "Lettuce", 98 | "Tomato", 99 | "Onion", 100 | "Pickles", 101 | "Signature sauce" 102 | ), 103 | calories = 520, 104 | preparationTime = 18, 105 | isVegetarian = false, 106 | reviews = listOf( 107 | ReviewItem( 108 | "Liam Taylor", 109 | 5.0f, 110 | "Best burger I've had in a long time! Juicy and perfectly cooked.", 111 | "Mar 1, 2025" 112 | ), 113 | ReviewItem( 114 | "Olivia Brown", 115 | 4.5f, 116 | "Great flavor profile. The sauce really makes it stand out.", 117 | "Feb 15, 2025" 118 | ) 119 | ) 120 | ), 121 | FoodItem( 122 | id = 3, 123 | name = "Hotdog", 124 | price = "$9.15", 125 | rating = 3.7f, 126 | imageRes = R.drawable.hotdog, 127 | description = "All-beef frankfurter served on a warm bun with your choice of ketchup, mustard, relish, and onions.", 128 | ingredients = listOf( 129 | "Beef frankfurter", 130 | "Hot dog bun", 131 | "Ketchup", 132 | "Mustard", 133 | "Relish", 134 | "Diced onions" 135 | ), 136 | calories = 290, 137 | preparationTime = 10, 138 | isVegetarian = false, 139 | reviews = listOf( 140 | ReviewItem( 141 | "Ethan Davis", 142 | 3.5f, 143 | "Standard hot dog, nothing extraordinary but satisfying.", 144 | "Feb 28, 2025" 145 | ), 146 | ReviewItem( 147 | "Ava Martinez", 148 | 4.0f, 149 | "Good quality hot dog. The bun was fresh which is important!", 150 | "Feb 10, 2025" 151 | ) 152 | ) 153 | ), 154 | FoodItem( 155 | id = 4, 156 | name = "Sandwich", 157 | price = "$8.00", 158 | rating = 4.3f, 159 | imageRes = R.drawable.sandwich, 160 | description = "Freshly baked multi-grain bread filled with turkey, avocado, bacon, lettuce, and tomato. Served with a side of chips.", 161 | ingredients = listOf( 162 | "Multi-grain bread", 163 | "Turkey", 164 | "Avocado", 165 | "Bacon", 166 | "Lettuce", 167 | "Tomato", 168 | "Mayo" 169 | ), 170 | calories = 425, 171 | preparationTime = 12, 172 | isVegetarian = false, 173 | reviews = listOf( 174 | ReviewItem( 175 | "Isabella Kim", 176 | 4.5f, 177 | "Fresh ingredients and good portion! I loved the avocado.", 178 | "Mar 3, 2025" 179 | ), 180 | ReviewItem( 181 | "Lucas Garcia", 182 | 4.0f, 183 | "Solid sandwich. The bread is really good quality.", 184 | "Feb 18, 2025" 185 | ) 186 | ) 187 | ), 188 | FoodItem( 189 | id = 5, 190 | name = "Sushi", 191 | price = "$15.50", 192 | rating = 4.6f, 193 | imageRes = R.drawable.sushi, 194 | description = "Freshly prepared sushi rolls with premium salmon, cucumber, and avocado. Served with wasabi, ginger, and soy sauce.", 195 | ingredients = listOf( 196 | "Sushi rice", 197 | "Nori seaweed", 198 | "Salmon", 199 | "Cucumber", 200 | "Avocado", 201 | "Wasabi", 202 | "Pickled ginger", 203 | "Soy sauce" 204 | ), 205 | calories = 350, 206 | preparationTime = 25, 207 | isVegetarian = false, 208 | reviews = listOf( 209 | ReviewItem( 210 | "Mia Wong", 211 | 5.0f, 212 | "Fresh fish and perfectly seasoned rice. Exceptional quality!", 213 | "Mar 2, 2025" 214 | ), 215 | ReviewItem( 216 | "William Lee", 217 | 4.5f, 218 | "Very fresh ingredients. The salmon practically melts in your mouth.", 219 | "Feb 20, 2025" 220 | ) 221 | ) 222 | ), 223 | FoodItem( 224 | id = 6, 225 | name = "Pizza", 226 | price = "$12.50", 227 | rating = 4.4f, 228 | imageRes = R.drawable.pizza, 229 | description = "Hand-tossed thin crust pizza with fresh mozzarella, basil, and our signature tomato sauce.", 230 | ingredients = listOf( 231 | "Pizza dough", 232 | "Tomato sauce", 233 | "Fresh mozzarella", 234 | "Basil", 235 | "Olive oil", 236 | "Italian herbs" 237 | ), 238 | calories = 285, 239 | preparationTime = 20, 240 | isVegetarian = true, 241 | reviews = listOf( 242 | ReviewItem( 243 | "Charlotte Romano", 244 | 4.5f, 245 | "Authentic Italian style with a great balance of flavors.", 246 | "Mar 5, 2025" 247 | ), 248 | ReviewItem( 249 | "Benjamin Scott", 250 | 4.0f, 251 | "Good pizza! Crust was perfectly crisp and chewy.", 252 | "Feb 25, 2025" 253 | ) 254 | ) 255 | ), 256 | FoodItem( 257 | id = 7, 258 | name = "Burrito", 259 | price = "$18.22", 260 | rating = 4.2f, 261 | imageRes = R.drawable.burrito, 262 | description = "Large flour tortilla filled with seasoned rice, black beans, grilled chicken, cheese, guacamole, and sour cream.", 263 | ingredients = listOf( 264 | "Flour tortilla", 265 | "Rice", 266 | "Black beans", 267 | "Grilled chicken", 268 | "Cheddar cheese", 269 | "Guacamole", 270 | "Sour cream", 271 | "Pico de gallo" 272 | ), 273 | calories = 680, 274 | preparationTime = 15, 275 | isVegetarian = false, 276 | reviews = listOf( 277 | ReviewItem( 278 | "Amelia Rodriguez", 279 | 4.0f, 280 | "Hefty portion and great flavor. The guacamole was fresh!", 281 | "Feb 28, 2025" 282 | ), 283 | ReviewItem( 284 | "Henry Thompson", 285 | 4.5f, 286 | "Perfect mix of ingredients. Very filling!", 287 | "Feb 15, 2025" 288 | ) 289 | ) 290 | ), 291 | FoodItem( 292 | id = 8, 293 | name = "Cake", 294 | price = "$5.99", 295 | rating = 4.5f, 296 | imageRes = R.drawable.bakery_cake_dessert, 297 | description = "Rich chocolate cake with layers of buttercream frosting. Perfect for dessert or celebrations.", 298 | ingredients = listOf( 299 | "Flour", 300 | "Sugar", 301 | "Cocoa powder", 302 | "Eggs", 303 | "Butter", 304 | "Milk", 305 | "Vanilla extract", 306 | "Buttercream frosting" 307 | ), 308 | calories = 450, 309 | preparationTime = 45, 310 | isVegetarian = true, 311 | reviews = listOf( 312 | ReviewItem( 313 | "Evelyn Baker", 314 | 5.0f, 315 | "Decadent and moist! The frosting is just perfect.", 316 | "Mar 6, 2025" 317 | ), 318 | ReviewItem( 319 | "Alexander White", 320 | 4.0f, 321 | "Delicious cake. Not too sweet, which I appreciate.", 322 | "Feb 23, 2025" 323 | ) 324 | ) 325 | ) 326 | ) 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample 2 | 3 | import android.graphics.Color 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.SystemBarStyle 7 | import androidx.activity.compose.setContent 8 | import androidx.activity.enableEdgeToEdge 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.material3.Scaffold 11 | import androidx.compose.ui.Modifier 12 | import com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.FoodOrderingScreen 13 | import com.binissa.sharedelementtransitionexample.ui.theme.SharedElementTransitionExampleTheme 14 | import dagger.hilt.android.AndroidEntryPoint 15 | 16 | @AndroidEntryPoint 17 | class MainActivity : ComponentActivity() { 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | enableEdgeToEdge( 21 | statusBarStyle = SystemBarStyle.light( 22 | Color.TRANSPARENT, 23 | Color.TRANSPARENT 24 | ) 25 | ) 26 | setContent { 27 | SharedElementTransitionExampleTheme { 28 | Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> 29 | FoodOrderingScreen() 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/MyApp.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class MyApp : Application() { 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/screens/food_ordering_screen/FoodOrderingScreen.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.screens.food_ordering_screen 2 | 3 | import android.util.Log 4 | import androidx.compose.animation.AnimatedContent 5 | import androidx.compose.animation.AnimatedContentScope 6 | import androidx.compose.animation.AnimatedVisibility 7 | import androidx.compose.animation.ExperimentalSharedTransitionApi 8 | import androidx.compose.animation.SharedTransitionLayout 9 | import androidx.compose.animation.SharedTransitionScope 10 | import androidx.compose.animation.SizeTransform 11 | import androidx.compose.animation.core.spring 12 | import androidx.compose.animation.core.tween 13 | import androidx.compose.animation.fadeIn 14 | import androidx.compose.animation.fadeOut 15 | import androidx.compose.animation.slideInVertically 16 | import androidx.compose.animation.slideOutVertically 17 | import androidx.compose.animation.togetherWith 18 | import androidx.compose.foundation.background 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.foundation.layout.PaddingValues 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.lazy.LazyColumn 23 | import androidx.compose.foundation.lazy.grid.GridCells 24 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 25 | import androidx.compose.foundation.lazy.grid.items 26 | import androidx.compose.foundation.lazy.items 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.collectAsState 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.runtime.remember 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.graphics.Color 33 | import androidx.compose.ui.unit.dp 34 | import androidx.hilt.navigation.compose.hiltViewModel 35 | import com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components.FoodItemCard 36 | import com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components.FoodItemDetailView 37 | import com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components.FoodItemListItem 38 | import com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components.TopAppBarForMainView 39 | import com.binissa.sharedelementtransitionexample.screens.home.FoodItem 40 | import dev.chrisbanes.haze.HazeState 41 | import dev.chrisbanes.haze.hazeSource 42 | 43 | @OptIn(ExperimentalSharedTransitionApi::class) 44 | @Composable 45 | fun FoodOrderingScreen(viewModel: FoodOrderingViewModel = hiltViewModel()) { 46 | // Observe the full UI state from the view model 47 | val state by viewModel.state.collectAsState() 48 | val hazeState = remember { HazeState() } 49 | 50 | SharedTransitionLayout { 51 | AnimatedContent( 52 | targetState = state.selectedItem, 53 | transitionSpec = { 54 | fadeIn(tween(300)) togetherWith fadeOut(tween(300)) using SizeTransform { _, _ -> 55 | spring(stiffness = 300f, dampingRatio = 0.8f) 56 | } 57 | }, 58 | label = "MainContentTransition" 59 | ) { selectedItem -> 60 | // The outer AnimatedContent scope will be used for detail transitions. 61 | val outerScope = this@AnimatedContent 62 | 63 | if (selectedItem == null) { 64 | // List view: the screen state is in list mode when no item is selected. 65 | FoodListView( 66 | foodItems = state.foodItems, 67 | isGridView = state.isGridView, 68 | onToggleView = { viewModel.toggleGridView() }, 69 | onItemClick = { viewModel.selectItem(it) }, 70 | onCartClick = { viewModel.toggleCartStatus(it) }, 71 | cartItemCount = state.cartItemCount, 72 | hazeState = hazeState, 73 | selectedItem = state.selectedItem, 74 | animatedVisibilityScope = outerScope, 75 | sharedTransitionScope = this@SharedTransitionLayout 76 | ) 77 | } else { 78 | // Detail view 79 | FoodItemDetailView( 80 | foodItem = selectedItem, 81 | onBackClick = { viewModel.returnToList() }, 82 | onUpdateQuantity = { item, quantity -> 83 | viewModel.updateQuantity( 84 | item, 85 | quantity 86 | ) 87 | }, 88 | onAddToCart = { }, 89 | animatedVisibilityScope = outerScope, 90 | sharedTransitionScope = this@SharedTransitionLayout 91 | ) 92 | } 93 | } 94 | } 95 | } 96 | 97 | @OptIn(ExperimentalSharedTransitionApi::class) 98 | @Composable 99 | fun FoodListView( 100 | foodItems: List, 101 | isGridView: Boolean, 102 | onToggleView: () -> Unit, 103 | onItemClick: (FoodItem) -> Unit, 104 | onCartClick: (FoodItem) -> Unit, 105 | cartItemCount: Int, 106 | hazeState: HazeState, 107 | selectedItem: FoodItem?, // Will be null in list mode 108 | animatedVisibilityScope: AnimatedContentScope, 109 | sharedTransitionScope: SharedTransitionScope 110 | ) { 111 | with(animatedVisibilityScope) { 112 | with(sharedTransitionScope) { 113 | Box( 114 | Modifier 115 | .fillMaxSize() 116 | .background(Color(0xFFF9F9F9)) 117 | ) { 118 | AnimatedContent( 119 | targetState = isGridView, 120 | transitionSpec = { 121 | fadeIn(tween(300)) + slideInVertically { it } togetherWith 122 | fadeOut(tween(300)) + slideOutVertically { -it } 123 | }, 124 | label = "GridListTransition" 125 | ) { gridView -> 126 | if (gridView) { 127 | LazyColumn( 128 | modifier = Modifier 129 | .fillMaxSize() 130 | .hazeSource(state = hazeState), 131 | contentPadding = PaddingValues(8.dp, top = 80.dp) 132 | ) { 133 | items(foodItems, key = { it.id }) { item -> 134 | Log.d("TAG", "FoodListView:" + 135 | "selectedItem:${selectedItem?.id} " + 136 | "item.id:${item.id}") 137 | val visibilityScope = 138 | if (selectedItem?.id == item.id) animatedVisibilityScope else this@AnimatedContent 139 | FoodItemListItem( 140 | foodItem = item, 141 | onItemClick = { onItemClick(item) }, 142 | onCartClick = { onCartClick(item) }, 143 | animatedVisibilityScope = visibilityScope, 144 | sharedTransitionScope = sharedTransitionScope 145 | ) 146 | } 147 | } 148 | } else { 149 | LazyVerticalGrid( 150 | columns = GridCells.Fixed(2), 151 | modifier = Modifier 152 | .fillMaxSize() 153 | .hazeSource(state = hazeState), 154 | contentPadding = PaddingValues(8.dp, top = 80.dp) 155 | ) { 156 | items(foodItems, key = { it.id }) { item -> 157 | val visibilityScope = 158 | if (selectedItem?.id == item.id) animatedVisibilityScope else this@AnimatedContent 159 | FoodItemCard( 160 | foodItem = item, 161 | onItemClick = { onItemClick(item) }, 162 | onCartClick = { onCartClick(item) }, 163 | animatedVisibilityScope = visibilityScope, 164 | sharedTransitionScope = sharedTransitionScope 165 | ) 166 | } 167 | } 168 | } 169 | } 170 | 171 | // Top App Bar remains outside the grid/list AnimatedContent. 172 | AnimatedVisibility( 173 | visible = true, 174 | enter = fadeIn(tween(300)) + slideInVertically { -it }, 175 | exit = fadeOut(tween(300)) + slideOutVertically { -it }, 176 | modifier = Modifier 177 | .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f) 178 | .animateEnterExit() 179 | ) { 180 | TopAppBarForMainView( 181 | isGridView = isGridView, 182 | onToggleView = onToggleView, 183 | cartItemCount = cartItemCount, 184 | hazeState = hazeState, 185 | animatedVisibilityScope = animatedVisibilityScope, 186 | sharedTransitionScope = sharedTransitionScope 187 | ) 188 | } 189 | } 190 | } 191 | } 192 | } -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/screens/food_ordering_screen/FoodOrderingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.screens.food_ordering_screen 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.binissa.sharedelementtransitionexample.screens.home.FoodItem 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.StateFlow 8 | import kotlinx.coroutines.flow.asStateFlow 9 | import kotlinx.coroutines.flow.update 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class FoodOrderingViewModel @Inject constructor() : ViewModel() { 14 | 15 | private val _state = MutableStateFlow(FoodOrderingViewState()) 16 | val state: StateFlow = _state.asStateFlow() 17 | 18 | fun toggleGridView() { 19 | _state.update { current -> 20 | current.copy(isGridView = !current.isGridView) 21 | } 22 | } 23 | 24 | fun selectItem(item: FoodItem) { 25 | _state.update { current -> 26 | current.copy(selectedItem = item) 27 | } 28 | } 29 | 30 | fun returnToList() { 31 | _state.update { current -> 32 | current.copy(selectedItem = null) 33 | } 34 | } 35 | 36 | fun toggleCartStatus(item: FoodItem) { 37 | _state.update { current -> 38 | val updatedItems = current.foodItems.map { 39 | if (it.id == item.id) { 40 | if (it.isInCart) { 41 | // Removing from cart 42 | it.copy(isInCart = false, quantity = 0) 43 | } else { 44 | // Adding to cart 45 | it.copy(isInCart = true, quantity = 1) 46 | } 47 | } else it 48 | } 49 | // Adjust the cart count based on the item toggle 50 | val currentItem = current.foodItems.find { it.id == item.id } 51 | val newCartCount = if (currentItem != null && currentItem.isInCart) { 52 | current.cartItemCount - currentItem.quantity 53 | } else { 54 | current.cartItemCount + 1 55 | } 56 | current.copy(foodItems = updatedItems, cartItemCount = newCartCount) 57 | } 58 | } 59 | 60 | fun updateQuantity(item: FoodItem, newQuantity: Int) { 61 | _state.update { current -> 62 | val updatedItems = current.foodItems.map { 63 | if (it.id == item.id) { 64 | it.copy(quantity = newQuantity) 65 | } else it 66 | } 67 | // Update the cart count by calculating the difference for this item. 68 | val currentItem = current.foodItems.find { it.id == item.id } 69 | val difference = if (currentItem != null) newQuantity - currentItem.quantity else 0 70 | current.copy( 71 | foodItems = updatedItems, 72 | cartItemCount = current.cartItemCount + difference 73 | ) 74 | } 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/screens/food_ordering_screen/FoodScreenState.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.screens.food_ordering_screen 2 | 3 | import com.binissa.sharedelementtransitionexample.screens.home.FoodItem 4 | import com.binissa.sharedelementtransitionexample.screens.home.sampleFoodItems 5 | 6 | data class FoodOrderingViewState( 7 | val foodItems: List = sampleFoodItems, 8 | val isGridView: Boolean = true, 9 | val cartItemCount: Int = 0, 10 | val selectedItem: FoodItem? = null 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/screens/food_ordering_screen/components/FoodItemCard.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components 2 | 3 | import androidx.compose.animation.AnimatedVisibilityScope 4 | import androidx.compose.animation.ExperimentalSharedTransitionApi 5 | import androidx.compose.animation.SharedTransitionScope 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.aspectRatio 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.size 16 | import androidx.compose.foundation.shape.CircleShape 17 | import androidx.compose.foundation.shape.RoundedCornerShape 18 | import androidx.compose.material.icons.Icons 19 | import androidx.compose.material.icons.filled.Star 20 | import androidx.compose.material3.CardDefaults 21 | import androidx.compose.material3.ElevatedCard 22 | import androidx.compose.material3.Icon 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Surface 25 | import androidx.compose.material3.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.layout.ContentScale 31 | import androidx.compose.ui.res.painterResource 32 | import androidx.compose.ui.text.font.FontWeight 33 | import androidx.compose.ui.text.style.TextOverflow 34 | import androidx.compose.ui.unit.dp 35 | import androidx.compose.ui.unit.sp 36 | import com.binissa.sharedelementtransitionexample.screens.home.FoodItem 37 | 38 | @OptIn(ExperimentalSharedTransitionApi::class) 39 | @Composable 40 | fun FoodItemCard( 41 | foodItem: FoodItem, 42 | onItemClick: () -> Unit, 43 | onCartClick: () -> Unit, 44 | animatedVisibilityScope: AnimatedVisibilityScope, 45 | sharedTransitionScope: SharedTransitionScope 46 | ) { 47 | with(sharedTransitionScope) { 48 | ElevatedCard( 49 | modifier = Modifier 50 | .padding(8.dp) 51 | .sharedBounds( 52 | rememberSharedContentState(key = "container-${foodItem.id}"), 53 | animatedVisibilityScope, 54 | clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(16.dp)) 55 | ) 56 | .clickable(onClick = onItemClick), 57 | elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp) 58 | ) { 59 | Column { 60 | Box { 61 | Image( 62 | painter = painterResource(id = foodItem.imageRes), 63 | contentDescription = foodItem.name, 64 | modifier = Modifier 65 | .fillMaxWidth() 66 | .aspectRatio(1f) 67 | .sharedElement( 68 | rememberSharedContentState(key = "image-${foodItem.id}"), 69 | animatedVisibilityScope, 70 | placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize 71 | ), 72 | contentScale = ContentScale.Crop 73 | ) 74 | 75 | // Vegetarian badge 76 | if (foodItem.isVegetarian) { 77 | Surface( 78 | modifier = Modifier 79 | .padding(8.dp) 80 | .size(24.dp) 81 | .align(Alignment.TopEnd) 82 | .sharedElement( 83 | state = rememberSharedContentState(key = "veg-badge-${foodItem.id}"), 84 | animatedVisibilityScope = animatedVisibilityScope 85 | ), 86 | shape = CircleShape, 87 | color = Color(0xFF388E3C) 88 | ) { 89 | Box(contentAlignment = Alignment.Center) { 90 | Text( 91 | text = "V", 92 | color = Color.White, 93 | fontWeight = FontWeight.Bold, 94 | fontSize = 14.sp 95 | ) 96 | } 97 | } 98 | } 99 | 100 | // Preparation time badge 101 | Surface( 102 | modifier = Modifier 103 | .padding(8.dp) 104 | .align(Alignment.BottomStart) 105 | .sharedElement( 106 | state = rememberSharedContentState(key = "prep-time-${foodItem.id}"), 107 | animatedVisibilityScope = animatedVisibilityScope 108 | ), 109 | shape = RoundedCornerShape(12.dp), 110 | color = Color(0x99000000) 111 | ) { 112 | Row( 113 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), 114 | verticalAlignment = Alignment.CenterVertically 115 | ) { 116 | Text( 117 | text = "${foodItem.preparationTime} min", 118 | color = Color.White, 119 | fontSize = 12.sp 120 | ) 121 | } 122 | } 123 | } 124 | 125 | Column(Modifier.padding(12.dp)) { 126 | Text( 127 | text = foodItem.name, 128 | fontWeight = FontWeight.Bold, 129 | modifier = Modifier.sharedElement( 130 | rememberSharedContentState(key = "title-${foodItem.id}"), 131 | animatedVisibilityScope 132 | ) 133 | ) 134 | 135 | Row( 136 | modifier = Modifier 137 | .fillMaxWidth() 138 | .padding(vertical = 4.dp), 139 | horizontalArrangement = Arrangement.SpaceBetween, 140 | verticalAlignment = Alignment.CenterVertically 141 | ) { 142 | Row( 143 | verticalAlignment = Alignment.CenterVertically, 144 | modifier = Modifier.sharedElement( 145 | state = rememberSharedContentState(key = "rating-${foodItem.id}"), 146 | animatedVisibilityScope = animatedVisibilityScope 147 | ) 148 | ) { 149 | Icon( 150 | imageVector = Icons.Default.Star, 151 | contentDescription = "Rating", 152 | tint = Color(0xFFFFC107), 153 | modifier = Modifier.size(16.dp) 154 | ) 155 | Text( 156 | text = foodItem.rating.toString(), 157 | fontSize = 12.sp, 158 | color = Color.Gray, 159 | modifier = Modifier.padding(start = 4.dp) 160 | ) 161 | } 162 | 163 | Text( 164 | text = foodItem.price, 165 | fontWeight = FontWeight.Bold, 166 | color = MaterialTheme.colorScheme.primary, 167 | modifier = Modifier.sharedElement( 168 | state = rememberSharedContentState(key = "price-${foodItem.id}"), 169 | animatedVisibilityScope = animatedVisibilityScope 170 | ) 171 | ) 172 | } 173 | 174 | Text( 175 | text = foodItem.description, 176 | fontSize = 12.sp, 177 | color = Color.Gray, 178 | maxLines = 2, 179 | overflow = TextOverflow.Ellipsis, 180 | modifier = Modifier 181 | .padding(top = 4.dp, bottom = 8.dp) 182 | .sharedElement( 183 | state = rememberSharedContentState(key = "desc-preview-${foodItem.id}"), 184 | animatedVisibilityScope = animatedVisibilityScope 185 | ) 186 | ) 187 | 188 | SelectionButton( 189 | isSelected = foodItem.isInCart, 190 | onClick = onCartClick, 191 | modifier = Modifier 192 | .align(Alignment.End) 193 | .sharedElement( 194 | state = rememberSharedContentState(key = "cart-button-${foodItem.id}"), 195 | animatedVisibilityScope = animatedVisibilityScope 196 | ) 197 | ) 198 | } 199 | } 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/screens/food_ordering_screen/components/FoodItemDetailView.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components 2 | 3 | import androidx.compose.animation.AnimatedVisibilityScope 4 | import androidx.compose.animation.ExperimentalSharedTransitionApi 5 | import androidx.compose.animation.SharedTransitionScope 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.aspectRatio 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.height 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.layout.width 19 | import androidx.compose.foundation.rememberScrollState 20 | import androidx.compose.foundation.shape.CircleShape 21 | import androidx.compose.foundation.shape.RoundedCornerShape 22 | import androidx.compose.foundation.verticalScroll 23 | import androidx.compose.material.icons.Icons 24 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 25 | import androidx.compose.material.icons.filled.ShoppingCart 26 | import androidx.compose.material.icons.filled.Star 27 | import androidx.compose.material.icons.outlined.FavoriteBorder 28 | import androidx.compose.material3.Button 29 | import androidx.compose.material3.Card 30 | import androidx.compose.material3.CardDefaults 31 | import androidx.compose.material3.ExperimentalMaterial3Api 32 | import androidx.compose.material3.Icon 33 | import androidx.compose.material3.IconButton 34 | import androidx.compose.material3.MaterialTheme 35 | import androidx.compose.material3.Scaffold 36 | import androidx.compose.material3.Surface 37 | import androidx.compose.material3.Text 38 | import androidx.compose.material3.TopAppBar 39 | import androidx.compose.material3.TopAppBarDefaults 40 | import androidx.compose.runtime.Composable 41 | import androidx.compose.runtime.getValue 42 | import androidx.compose.runtime.mutableIntStateOf 43 | import androidx.compose.runtime.remember 44 | import androidx.compose.runtime.setValue 45 | import androidx.compose.ui.Alignment 46 | import androidx.compose.ui.Modifier 47 | import androidx.compose.ui.graphics.Color 48 | import androidx.compose.ui.layout.ContentScale 49 | import androidx.compose.ui.res.painterResource 50 | import androidx.compose.ui.text.font.FontWeight 51 | import androidx.compose.ui.unit.dp 52 | import androidx.compose.ui.unit.sp 53 | import com.binissa.sharedelementtransitionexample.screens.home.FoodItem 54 | import dev.chrisbanes.haze.ExperimentalHazeApi 55 | import dev.chrisbanes.haze.HazeInputScale 56 | import dev.chrisbanes.haze.HazeProgressive 57 | import dev.chrisbanes.haze.HazeState 58 | import dev.chrisbanes.haze.hazeEffect 59 | import dev.chrisbanes.haze.hazeSource 60 | 61 | @OptIn( 62 | ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class, 63 | ExperimentalHazeApi::class 64 | ) 65 | @Composable 66 | fun FoodItemDetailView( 67 | foodItem: FoodItem, 68 | onBackClick: () -> Unit, 69 | onUpdateQuantity: (FoodItem, Int) -> Unit, 70 | onAddToCart: () -> Unit, 71 | animatedVisibilityScope: AnimatedVisibilityScope, 72 | sharedTransitionScope: SharedTransitionScope 73 | ) { 74 | var quantity by remember { mutableIntStateOf(foodItem.quantity) } 75 | val scrollState = rememberScrollState() 76 | val hazeState = remember { HazeState() } 77 | 78 | with(sharedTransitionScope) { 79 | Scaffold( 80 | topBar = { 81 | TopAppBar( 82 | title = { 83 | Text( 84 | text = foodItem.name, 85 | modifier = Modifier.sharedElement( 86 | rememberSharedContentState(key = "title-${foodItem.id}"), 87 | animatedVisibilityScope 88 | ) 89 | ) 90 | }, 91 | navigationIcon = { 92 | IconButton(onClick = onBackClick) { 93 | Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") 94 | } 95 | }, 96 | actions = { 97 | IconButton(onClick = { /* Toggle favorite */ }) { 98 | Icon(Icons.Outlined.FavoriteBorder, contentDescription = "Favorite") 99 | } 100 | }, 101 | colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), 102 | modifier = Modifier.hazeEffect(state = hazeState) { 103 | backgroundColor = Color.Transparent 104 | inputScale = HazeInputScale.Auto 105 | progressive = 106 | HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) 107 | 108 | } 109 | 110 | ) 111 | }, 112 | bottomBar = { 113 | Surface( 114 | color = MaterialTheme.colorScheme.surface, 115 | shadowElevation = 8.dp 116 | ) { 117 | Row( 118 | modifier = Modifier 119 | .fillMaxWidth() 120 | .padding(16.dp), 121 | horizontalArrangement = Arrangement.SpaceBetween, 122 | verticalAlignment = Alignment.CenterVertically 123 | ) { 124 | Row( 125 | verticalAlignment = Alignment.CenterVertically 126 | ) { 127 | Text( 128 | text = "Quantity:", 129 | fontWeight = FontWeight.Medium 130 | ) 131 | 132 | IconButton( 133 | onClick = { 134 | if (quantity > 0) quantity-- 135 | }, 136 | modifier = Modifier.size(36.dp) 137 | ) { 138 | Text( 139 | text = "−", 140 | fontWeight = FontWeight.Bold, 141 | fontSize = 20.sp 142 | ) 143 | } 144 | 145 | Text( 146 | text = quantity.toString(), 147 | fontWeight = FontWeight.Bold, 148 | modifier = Modifier.padding(horizontal = 8.dp) 149 | ) 150 | 151 | IconButton( 152 | onClick = { quantity++ }, 153 | modifier = Modifier.size(36.dp) 154 | ) { 155 | Text( 156 | text = "+", 157 | fontWeight = FontWeight.Bold, 158 | fontSize = 20.sp 159 | ) 160 | } 161 | } 162 | 163 | Button( 164 | onClick = { 165 | onUpdateQuantity(foodItem, quantity) 166 | onAddToCart() 167 | }, 168 | modifier = Modifier.sharedElement( 169 | state = rememberSharedContentState(key = "cart-button-${foodItem.id}"), 170 | animatedVisibilityScope = animatedVisibilityScope 171 | ) 172 | ) { 173 | Icon( 174 | Icons.Default.ShoppingCart, 175 | contentDescription = null, 176 | modifier = Modifier.size(16.dp) 177 | ) 178 | Spacer(modifier = Modifier.width(8.dp)) 179 | Text( 180 | text = if (foodItem.isInCart) "Update Cart" else "Add to Cart" 181 | ) 182 | } 183 | } 184 | } 185 | } 186 | ) { paddingValues -> 187 | Column( 188 | modifier = Modifier 189 | .fillMaxSize() 190 | // .padding(paddingValues) 191 | .verticalScroll(scrollState) 192 | .sharedBounds( 193 | rememberSharedContentState(key = "container-${foodItem.id}"), 194 | animatedVisibilityScope, 195 | clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(16.dp)) 196 | ) 197 | .hazeSource(hazeState) 198 | ) { 199 | // Hero image with badges 200 | Box( 201 | Modifier 202 | .fillMaxWidth() 203 | .padding(paddingValues) 204 | .aspectRatio(1f) 205 | ) { 206 | Image( 207 | painter = painterResource(id = foodItem.imageRes), 208 | contentDescription = null, 209 | modifier = Modifier 210 | .fillMaxSize() 211 | .sharedElement( 212 | rememberSharedContentState(key = "image-${foodItem.id}"), 213 | animatedVisibilityScope, 214 | placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize 215 | ), 216 | contentScale = ContentScale.Crop 217 | ) 218 | 219 | // Vegetarian badge 220 | if (foodItem.isVegetarian) { 221 | Surface( 222 | modifier = Modifier 223 | .padding(16.dp) 224 | .size(32.dp) 225 | .align(Alignment.TopEnd) 226 | .sharedElement( 227 | state = rememberSharedContentState(key = "veg-badge-${foodItem.id}"), 228 | animatedVisibilityScope = animatedVisibilityScope 229 | ), 230 | shape = CircleShape, 231 | color = Color(0xFF388E3C) 232 | ) { 233 | Box(contentAlignment = Alignment.Center) { 234 | Text( 235 | text = "V", 236 | color = Color.White, 237 | fontWeight = FontWeight.Bold, 238 | fontSize = 18.sp 239 | ) 240 | } 241 | } 242 | } 243 | 244 | // Preparation time badge 245 | Surface( 246 | modifier = Modifier 247 | .padding(16.dp) 248 | .align(Alignment.BottomStart) 249 | .sharedElement( 250 | state = rememberSharedContentState(key = "prep-time-${foodItem.id}"), 251 | animatedVisibilityScope = animatedVisibilityScope 252 | ), 253 | shape = RoundedCornerShape(16.dp), 254 | color = Color(0x99000000) 255 | ) { 256 | Row( 257 | modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), 258 | verticalAlignment = Alignment.CenterVertically 259 | ) { 260 | Text( 261 | text = "Prep time: ${foodItem.preparationTime} min", 262 | color = Color.White, 263 | fontSize = 14.sp, 264 | fontWeight = FontWeight.Medium 265 | ) 266 | } 267 | } 268 | } 269 | 270 | // Item details 271 | Column(Modifier.padding(16.dp)) { 272 | // Header section with price and rating 273 | Row( 274 | modifier = Modifier.fillMaxWidth(), 275 | horizontalArrangement = Arrangement.SpaceBetween, 276 | verticalAlignment = Alignment.CenterVertically 277 | ) { 278 | Text( 279 | text = foodItem.price, 280 | style = MaterialTheme.typography.headlineMedium, 281 | color = MaterialTheme.colorScheme.primary, 282 | fontWeight = FontWeight.Bold, 283 | modifier = Modifier.sharedElement( 284 | state = rememberSharedContentState(key = "price-${foodItem.id}"), 285 | animatedVisibilityScope = animatedVisibilityScope 286 | ) 287 | ) 288 | 289 | Row( 290 | verticalAlignment = Alignment.CenterVertically, 291 | modifier = Modifier.sharedElement( 292 | state = rememberSharedContentState(key = "rating-${foodItem.id}"), 293 | animatedVisibilityScope = animatedVisibilityScope 294 | ) 295 | ) { 296 | Icon( 297 | imageVector = Icons.Default.Star, 298 | contentDescription = "Rating", 299 | tint = Color(0xFFFFC107), 300 | modifier = Modifier.size(24.dp) 301 | ) 302 | Text( 303 | text = foodItem.rating.toString(), 304 | fontSize = 16.sp, 305 | fontWeight = FontWeight.Bold, 306 | modifier = Modifier.padding(start = 4.dp) 307 | ) 308 | Text( 309 | text = "(${foodItem.reviews.size} reviews)", 310 | fontSize = 14.sp, 311 | color = Color.Gray, 312 | modifier = Modifier.padding(start = 4.dp) 313 | ) 314 | } 315 | } 316 | 317 | Spacer(modifier = Modifier.height(16.dp)) 318 | 319 | // Description section 320 | Text( 321 | text = "Description", 322 | style = MaterialTheme.typography.titleMedium, 323 | fontWeight = FontWeight.Bold 324 | ) 325 | 326 | Spacer(modifier = Modifier.height(8.dp)) 327 | 328 | Text( 329 | text = foodItem.description, 330 | style = MaterialTheme.typography.bodyMedium, 331 | color = Color.DarkGray, 332 | modifier = Modifier.sharedElement( 333 | state = rememberSharedContentState(key = "desc-preview-${foodItem.id}"), 334 | animatedVisibilityScope = animatedVisibilityScope 335 | ) 336 | ) 337 | 338 | Spacer(modifier = Modifier.height(16.dp)) 339 | 340 | // Nutritional info 341 | Text( 342 | text = "Nutritional Information", 343 | style = MaterialTheme.typography.titleMedium, 344 | fontWeight = FontWeight.Bold 345 | ) 346 | 347 | Spacer(modifier = Modifier.height(8.dp)) 348 | 349 | Row( 350 | modifier = Modifier.fillMaxWidth(), 351 | horizontalArrangement = Arrangement.SpaceEvenly 352 | ) { 353 | NutritionInfoItem( 354 | value = foodItem.calories.toString(), 355 | unit = "cal", 356 | name = "Calories" 357 | ) 358 | 359 | // Mock nutritional values 360 | NutritionInfoItem( 361 | value = "${(foodItem.calories * 0.045).toInt()}", 362 | unit = "g", 363 | name = "Protein" 364 | ) 365 | 366 | NutritionInfoItem( 367 | value = "${(foodItem.calories * 0.033).toInt()}", 368 | unit = "g", 369 | name = "Carbs" 370 | ) 371 | 372 | NutritionInfoItem( 373 | value = "${(foodItem.calories * 0.022).toInt()}", 374 | unit = "g", 375 | name = "Fat" 376 | ) 377 | } 378 | 379 | Spacer(modifier = Modifier.height(16.dp)) 380 | 381 | // Ingredients section 382 | Text( 383 | text = "Ingredients", 384 | style = MaterialTheme.typography.titleMedium, 385 | fontWeight = FontWeight.Bold 386 | ) 387 | 388 | Spacer(modifier = Modifier.height(8.dp)) 389 | 390 | foodItem.ingredients.forEachIndexed { index, ingredient -> 391 | Text( 392 | text = "• $ingredient", 393 | style = MaterialTheme.typography.bodyMedium, 394 | color = Color.DarkGray, 395 | modifier = Modifier.padding(vertical = 2.dp) 396 | ) 397 | } 398 | 399 | Spacer(modifier = Modifier.height(16.dp)) 400 | 401 | // Reviews section 402 | Text( 403 | text = "Customer Reviews", 404 | style = MaterialTheme.typography.titleMedium, 405 | fontWeight = FontWeight.Bold 406 | ) 407 | 408 | Spacer(modifier = Modifier.height(8.dp)) 409 | 410 | foodItem.reviews.forEach { review -> 411 | Card( 412 | modifier = Modifier 413 | .fillMaxWidth() 414 | .padding(vertical = 4.dp), 415 | colors = CardDefaults.cardColors( 416 | containerColor = MaterialTheme.colorScheme.surface 417 | ) 418 | ) { 419 | Column( 420 | modifier = Modifier.padding(12.dp) 421 | ) { 422 | Row( 423 | modifier = Modifier.fillMaxWidth(), 424 | horizontalArrangement = Arrangement.SpaceBetween, 425 | verticalAlignment = Alignment.CenterVertically 426 | ) { 427 | Text( 428 | text = review.reviewerName, 429 | fontWeight = FontWeight.Medium 430 | ) 431 | 432 | Text( 433 | text = review.date, 434 | style = MaterialTheme.typography.bodySmall, 435 | color = Color.Gray 436 | ) 437 | } 438 | 439 | Spacer(modifier = Modifier.height(4.dp)) 440 | 441 | Row(verticalAlignment = Alignment.CenterVertically) { 442 | Icon( 443 | imageVector = Icons.Default.Star, 444 | contentDescription = "Rating", 445 | tint = Color(0xFFFFC107), 446 | modifier = Modifier.size(16.dp) 447 | ) 448 | 449 | Text( 450 | text = review.rating.toString(), 451 | fontSize = 14.sp, 452 | fontWeight = FontWeight.Bold, 453 | modifier = Modifier.padding(start = 4.dp) 454 | ) 455 | } 456 | 457 | Spacer(modifier = Modifier.height(4.dp)) 458 | 459 | Text( 460 | text = review.comment, 461 | style = MaterialTheme.typography.bodyMedium 462 | ) 463 | } 464 | } 465 | } 466 | 467 | // Add some space at the bottom for the bottom bar 468 | Spacer(modifier = Modifier.height(24.dp)) 469 | } 470 | } 471 | } 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/screens/food_ordering_screen/components/FoodItemListItem.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components 2 | 3 | import androidx.compose.animation.AnimatedVisibilityScope 4 | import androidx.compose.animation.ExperimentalSharedTransitionApi 5 | import androidx.compose.animation.SharedTransitionScope 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.offset 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.layout.size 17 | import androidx.compose.foundation.layout.width 18 | import androidx.compose.foundation.shape.CircleShape 19 | import androidx.compose.foundation.shape.RoundedCornerShape 20 | import androidx.compose.material.icons.Icons 21 | import androidx.compose.material.icons.filled.Star 22 | import androidx.compose.material3.CardDefaults 23 | import androidx.compose.material3.ElevatedCard 24 | import androidx.compose.material3.Icon 25 | import androidx.compose.material3.MaterialTheme 26 | import androidx.compose.material3.Surface 27 | import androidx.compose.material3.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.draw.clip 32 | import androidx.compose.ui.graphics.Color 33 | import androidx.compose.ui.layout.ContentScale 34 | import androidx.compose.ui.res.painterResource 35 | import androidx.compose.ui.text.font.FontWeight 36 | import androidx.compose.ui.text.style.TextOverflow 37 | import androidx.compose.ui.unit.dp 38 | import androidx.compose.ui.unit.sp 39 | import com.binissa.sharedelementtransitionexample.screens.home.FoodItem 40 | 41 | @OptIn(ExperimentalSharedTransitionApi::class) 42 | @Composable 43 | fun FoodItemListItem( 44 | foodItem: FoodItem, 45 | onItemClick: () -> Unit, 46 | onCartClick: () -> Unit, 47 | animatedVisibilityScope: AnimatedVisibilityScope, 48 | sharedTransitionScope: SharedTransitionScope, 49 | modifier: Modifier = Modifier 50 | ) { 51 | with(sharedTransitionScope) { 52 | ElevatedCard( 53 | modifier = modifier 54 | .fillMaxWidth() 55 | .padding(8.dp) 56 | .sharedBounds( 57 | sharedContentState = rememberSharedContentState(key = "container-${foodItem.id}"), 58 | animatedVisibilityScope = animatedVisibilityScope, 59 | clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(16.dp)) 60 | ), 61 | elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp) 62 | ) { 63 | Row( 64 | modifier = Modifier 65 | .fillMaxWidth() 66 | .clickable(onClick = onItemClick) 67 | .padding(8.dp), 68 | verticalAlignment = Alignment.CenterVertically 69 | ) { 70 | Box { 71 | Image( 72 | painter = painterResource(id = foodItem.imageRes), 73 | contentDescription = foodItem.name, 74 | modifier = Modifier 75 | .size(80.dp) 76 | .clip(RoundedCornerShape(12.dp)) 77 | .sharedElement( 78 | state = rememberSharedContentState(key = "image-${foodItem.id}"), 79 | animatedVisibilityScope = animatedVisibilityScope, 80 | placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize 81 | ), 82 | contentScale = ContentScale.Crop 83 | ) 84 | 85 | if (foodItem.isVegetarian) { 86 | Surface( 87 | modifier = Modifier 88 | .size(24.dp) 89 | .align(Alignment.TopEnd) 90 | .offset(x = (-4).dp, y = 4.dp) 91 | .sharedElement( 92 | state = rememberSharedContentState(key = "veg-badge-${foodItem.id}"), 93 | animatedVisibilityScope = animatedVisibilityScope 94 | ), 95 | shape = CircleShape, 96 | color = Color(0xFF388E3C) 97 | ) { 98 | Box(contentAlignment = Alignment.Center) { 99 | Text( 100 | text = "V", 101 | color = Color.White, 102 | fontWeight = FontWeight.Bold, 103 | fontSize = 14.sp 104 | ) 105 | } 106 | } 107 | } 108 | } 109 | 110 | Spacer(modifier = Modifier.width(16.dp)) 111 | 112 | Column( 113 | modifier = Modifier.weight(1f) 114 | ) { 115 | Text( 116 | text = foodItem.name, 117 | fontWeight = FontWeight.Bold, 118 | fontSize = 16.sp, 119 | modifier = Modifier.sharedElement( 120 | state = rememberSharedContentState(key = "title-${foodItem.id}"), 121 | animatedVisibilityScope = animatedVisibilityScope 122 | ) 123 | ) 124 | 125 | Row( 126 | verticalAlignment = Alignment.CenterVertically, 127 | modifier = Modifier.sharedElement( 128 | state = rememberSharedContentState(key = "rating-${foodItem.id}"), 129 | animatedVisibilityScope = animatedVisibilityScope 130 | ) 131 | ) { 132 | Icon( 133 | imageVector = Icons.Default.Star, 134 | contentDescription = "Rating", 135 | tint = Color(0xFFFFC107), 136 | modifier = Modifier.size(16.dp) 137 | ) 138 | Text( 139 | text = foodItem.rating.toString(), 140 | fontSize = 12.sp, 141 | color = Color.Gray, 142 | modifier = Modifier.padding(start = 4.dp) 143 | ) 144 | } 145 | 146 | Text( 147 | text = foodItem.description, 148 | fontSize = 12.sp, 149 | color = Color.Gray, 150 | maxLines = 1, 151 | overflow = TextOverflow.Ellipsis, 152 | modifier = Modifier.sharedElement( 153 | state = rememberSharedContentState(key = "desc-preview-${foodItem.id}"), 154 | animatedVisibilityScope = animatedVisibilityScope 155 | ) 156 | ) 157 | } 158 | 159 | Column( 160 | horizontalAlignment = Alignment.End 161 | ) { 162 | Text( 163 | text = foodItem.price, 164 | fontWeight = FontWeight.Bold, 165 | color = MaterialTheme.colorScheme.primary, 166 | modifier = Modifier.sharedElement( 167 | state = rememberSharedContentState(key = "price-${foodItem.id}"), 168 | animatedVisibilityScope = animatedVisibilityScope 169 | ) 170 | ) 171 | 172 | Spacer(modifier = Modifier.height(8.dp)) 173 | 174 | SelectionButton( 175 | isSelected = foodItem.isInCart, 176 | onClick = onCartClick, 177 | modifier = Modifier.sharedElement( 178 | state = rememberSharedContentState(key = "cart-button-${foodItem.id}"), 179 | animatedVisibilityScope = animatedVisibilityScope 180 | ) 181 | ) 182 | } 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/screens/food_ordering_screen/components/NutritionInfoItem.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.unit.sp 10 | 11 | @Composable 12 | fun NutritionInfoItem( 13 | value: String, 14 | unit: String, 15 | name: String 16 | ) { 17 | Column( 18 | horizontalAlignment = Alignment.CenterHorizontally 19 | ) { 20 | Text( 21 | text = value, 22 | fontWeight = FontWeight.Bold, 23 | fontSize = 16.sp 24 | ) 25 | 26 | Text( 27 | text = unit, 28 | fontSize = 12.sp, 29 | color = Color.Gray 30 | ) 31 | 32 | Text( 33 | text = name, 34 | fontSize = 12.sp, 35 | color = Color.Gray 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/screens/food_ordering_screen/components/SelectionButton.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.animateColor 5 | import androidx.compose.animation.core.FastOutSlowInEasing 6 | import androidx.compose.animation.core.animateFloat 7 | import androidx.compose.animation.core.tween 8 | import androidx.compose.animation.core.updateTransition 9 | import androidx.compose.animation.fadeIn 10 | import androidx.compose.animation.fadeOut 11 | import androidx.compose.animation.scaleIn 12 | import androidx.compose.animation.scaleOut 13 | import androidx.compose.animation.togetherWith 14 | import androidx.compose.foundation.background 15 | import androidx.compose.foundation.clickable 16 | import androidx.compose.foundation.layout.Box 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.shape.CircleShape 19 | import androidx.compose.material.icons.Icons 20 | import androidx.compose.material.icons.filled.Add 21 | import androidx.compose.material.icons.filled.Check 22 | import androidx.compose.material3.Icon 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.draw.clip 29 | import androidx.compose.ui.draw.scale 30 | import androidx.compose.ui.graphics.Color 31 | import androidx.compose.ui.unit.Dp 32 | import androidx.compose.ui.unit.dp 33 | 34 | @Composable 35 | fun SelectionButton( 36 | isSelected: Boolean, 37 | onClick: () -> Unit, 38 | modifier: Modifier = Modifier, 39 | size: Dp = 36.dp, 40 | iconSize: Dp = 20.dp 41 | ) { 42 | val transition = updateTransition(targetState = isSelected, label = "SelectionTransition") 43 | 44 | val scale by transition.animateFloat( 45 | transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, 46 | label = "ScaleAnimation" 47 | ) { if (it) 1.1f else 1f } 48 | 49 | val backgroundColor by transition.animateColor( 50 | transitionSpec = { tween(300, easing = FastOutSlowInEasing) }, 51 | label = "ColorAnimation" 52 | ) { if (it) MaterialTheme.colorScheme.primary else Color.LightGray.copy(alpha = 0.3f) } 53 | 54 | Box( 55 | modifier = modifier 56 | .size(size) 57 | .scale(scale) 58 | .clip(CircleShape) 59 | .background(backgroundColor) 60 | .clickable(onClick = onClick), 61 | contentAlignment = Alignment.Center 62 | ) { 63 | AnimatedContent( 64 | targetState = isSelected, 65 | transitionSpec = { 66 | (scaleIn(initialScale = 0.1f, animationSpec = tween(200)) + 67 | fadeIn(animationSpec = tween(200))) togetherWith 68 | (scaleOut(targetScale = 2f, animationSpec = tween(200)) + 69 | fadeOut(animationSpec = tween(200))) 70 | }, 71 | label = "IconAnimation" 72 | ) { targetState -> 73 | Icon( 74 | imageVector = if (targetState) Icons.Default.Check else Icons.Default.Add, 75 | contentDescription = if (targetState) "Selected" else "Add", 76 | tint = if (targetState) Color.White else Color.Black, 77 | modifier = Modifier.size(iconSize) 78 | ) 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/screens/food_ordering_screen/components/TopAppBarForMainView.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.screens.food_ordering_screen.components 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.animation.AnimatedVisibilityScope 5 | import androidx.compose.animation.ExperimentalSharedTransitionApi 6 | import androidx.compose.animation.SharedTransitionScope 7 | import androidx.compose.animation.animateColor 8 | import androidx.compose.animation.core.FastOutSlowInEasing 9 | import androidx.compose.animation.core.animateFloat 10 | import androidx.compose.animation.core.spring 11 | import androidx.compose.animation.core.tween 12 | import androidx.compose.animation.core.updateTransition 13 | import androidx.compose.animation.fadeIn 14 | import androidx.compose.animation.fadeOut 15 | import androidx.compose.animation.scaleIn 16 | import androidx.compose.animation.scaleOut 17 | import androidx.compose.animation.slideInVertically 18 | import androidx.compose.animation.slideOutVertically 19 | import androidx.compose.animation.togetherWith 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.size 22 | import androidx.compose.material.icons.Icons 23 | import androidx.compose.material.icons.automirrored.filled.List 24 | import androidx.compose.material.icons.filled.Menu 25 | import androidx.compose.material.icons.filled.ShoppingCart 26 | import androidx.compose.material3.Badge 27 | import androidx.compose.material3.BadgedBox 28 | import androidx.compose.material3.ExperimentalMaterial3Api 29 | import androidx.compose.material3.Icon 30 | import androidx.compose.material3.IconButton 31 | import androidx.compose.material3.LocalContentColor 32 | import androidx.compose.material3.MaterialTheme 33 | import androidx.compose.material3.Text 34 | import androidx.compose.material3.TopAppBar 35 | import androidx.compose.material3.TopAppBarDefaults 36 | import androidx.compose.runtime.Composable 37 | import androidx.compose.runtime.LaunchedEffect 38 | import androidx.compose.runtime.getValue 39 | import androidx.compose.runtime.mutableIntStateOf 40 | import androidx.compose.runtime.remember 41 | import androidx.compose.runtime.setValue 42 | import androidx.compose.ui.Alignment 43 | import androidx.compose.ui.Modifier 44 | import androidx.compose.ui.draw.scale 45 | import androidx.compose.ui.graphics.Color 46 | import androidx.compose.ui.graphics.graphicsLayer 47 | import androidx.compose.ui.unit.dp 48 | import com.binissa.sharedelementtransitionexample.ui.theme.Grid2x2 49 | import dev.chrisbanes.haze.ExperimentalHazeApi 50 | import dev.chrisbanes.haze.HazeInputScale 51 | import dev.chrisbanes.haze.HazeProgressive 52 | import dev.chrisbanes.haze.HazeState 53 | import dev.chrisbanes.haze.hazeEffect 54 | import kotlinx.coroutines.delay 55 | import kotlin.math.sin 56 | 57 | @OptIn( 58 | ExperimentalSharedTransitionApi::class, 59 | ExperimentalMaterial3Api::class, 60 | ExperimentalHazeApi::class 61 | ) 62 | @Composable 63 | fun TopAppBarForMainView( 64 | isGridView: Boolean, 65 | onToggleView: () -> Unit, 66 | cartItemCount: Int, 67 | hazeState: HazeState, 68 | animatedVisibilityScope: AnimatedVisibilityScope, 69 | sharedTransitionScope: SharedTransitionScope 70 | ) { 71 | // Track previous cart count for animation purposes 72 | var previousCartCount by remember { mutableIntStateOf(0) } 73 | val cartCountChanged = cartItemCount != previousCartCount 74 | 75 | // Animate badge when cart count changes 76 | val badgeTransition = updateTransition(targetState = cartCountChanged, label = "BadgeAnimation") 77 | val badgeScale by badgeTransition.animateFloat( 78 | transitionSpec = { 79 | if (targetState && cartItemCount > previousCartCount) { 80 | // Bouncy animation when adding items 81 | spring(stiffness = 400f, dampingRatio = 0.4f) 82 | } else { 83 | tween(300, easing = FastOutSlowInEasing) 84 | } 85 | }, label = "BadgeScale" 86 | ) { if (it) 1.3f else 1f } 87 | 88 | // Update previous count after animation completes 89 | LaunchedEffect(cartItemCount) { 90 | delay(300) 91 | previousCartCount = cartItemCount 92 | } 93 | 94 | // View mode transition for grid/list toggle 95 | val viewModeTransition = 96 | updateTransition(targetState = isGridView, label = "ViewModeTransition") 97 | 98 | // Grid/List toggle animation effects 99 | val iconRotation by viewModeTransition.animateFloat( 100 | transitionSpec = { tween(300, easing = FastOutSlowInEasing) }, label = "IconRotation" 101 | ) { if (it) 0f else 180f } 102 | 103 | val iconColor by viewModeTransition.animateColor( 104 | transitionSpec = { tween(300) }, label = "IconColor" 105 | ) { if (it) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary } 106 | 107 | with(sharedTransitionScope) { 108 | with(animatedVisibilityScope) { 109 | TopAppBar(title = { 110 | Text( 111 | "Food Delivery", 112 | modifier = Modifier.animateEnterExit(enter = fadeIn() + slideInVertically { -50 }, 113 | exit = fadeOut() + slideOutVertically { -50 }) 114 | ) 115 | }, 116 | navigationIcon = { 117 | IconButton( 118 | onClick = { /* Open drawer */ }, modifier = Modifier.animateEnterExit( 119 | enter = fadeIn() + scaleIn(initialScale = 0.8f), 120 | exit = fadeOut() + scaleOut(targetScale = 0.8f) 121 | ) 122 | ) { 123 | Icon(Icons.Default.Menu, contentDescription = "Menu") 124 | } 125 | }, 126 | actions = { 127 | // Cart icon with animated badge 128 | Box( 129 | contentAlignment = Alignment.Center, modifier = Modifier.animateEnterExit( 130 | enter = fadeIn() + scaleIn(initialScale = 0.8f), 131 | exit = fadeOut() + scaleOut(targetScale = 0.8f) 132 | ) 133 | ) { 134 | BadgedBox(badge = { 135 | if (cartItemCount > 0) { 136 | Badge( 137 | modifier = Modifier.scale(badgeScale) 138 | ) { 139 | Text(text = cartItemCount.toString(), 140 | modifier = Modifier.graphicsLayer { 141 | // Add slight wobble effect on change 142 | if (cartCountChanged) { 143 | rotationZ = 144 | sin(System.currentTimeMillis() % 1000 / 100f) * 5f 145 | } 146 | }) 147 | } 148 | } 149 | }) { 150 | IconButton( 151 | onClick = { /* Open cart */ }, 152 | ) { 153 | Icon( 154 | Icons.Default.ShoppingCart, 155 | contentDescription = "Shopping Cart", 156 | tint = if (cartItemCount > 0) MaterialTheme.colorScheme.primary 157 | else LocalContentColor.current 158 | ) 159 | } 160 | } 161 | } 162 | 163 | // Enhanced Grid/List toggle with rotation and icon swap animation 164 | IconButton( 165 | onClick = onToggleView, modifier = Modifier.animateEnterExit( 166 | enter = fadeIn() + scaleIn(initialScale = 0.8f), 167 | exit = fadeOut() + scaleOut(targetScale = 0.8f) 168 | ) 169 | ) { 170 | Box(contentAlignment = Alignment.Center, 171 | modifier = Modifier 172 | .size(24.dp) 173 | .graphicsLayer { 174 | rotationY = iconRotation 175 | cameraDistance = 12 * density 176 | }) { 177 | AnimatedContent( 178 | targetState = isGridView, transitionSpec = { 179 | (scaleIn( 180 | initialScale = 0.0f, animationSpec = tween(300) 181 | ) + fadeIn(animationSpec = tween(200))) togetherWith (scaleOut( 182 | targetScale = 0.0f, animationSpec = tween(300) 183 | ) + fadeOut(animationSpec = tween(200))) 184 | }, label = "GridListIconTransition" 185 | ) { targetState -> 186 | Icon( 187 | imageVector = if (targetState) Icons.AutoMirrored.Filled.List 188 | else Grid2x2, 189 | contentDescription = if (targetState) "Switch to List" 190 | else "Switch to Grid", 191 | tint = iconColor, 192 | modifier = Modifier.scale(1.2f) 193 | ) 194 | } 195 | } 196 | } 197 | }, 198 | colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), 199 | modifier = Modifier 200 | .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f) 201 | .animateEnterExit(fadeIn(), fadeOut()) 202 | .hazeEffect(state = hazeState) { 203 | backgroundColor = Color.Transparent 204 | inputScale = HazeInputScale.Auto 205 | progressive = 206 | HazeProgressive.verticalGradient(startIntensity = 2f, endIntensity = 0f) 207 | 208 | }) 209 | } 210 | } 211 | } 212 | 213 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/ui/theme/Grid2x2.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap 7 | import androidx.compose.ui.graphics.StrokeJoin 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.unit.dp 11 | 12 | public val Grid2x2: ImageVector 13 | get() { 14 | if (_Grid2x2 != null) { 15 | return _Grid2x2!! 16 | } 17 | _Grid2x2 = ImageVector.Builder( 18 | name = "Grid2x2", 19 | defaultWidth = 24.dp, 20 | defaultHeight = 24.dp, 21 | viewportWidth = 24f, 22 | viewportHeight = 24f 23 | ).apply { 24 | path( 25 | fill = null, 26 | fillAlpha = 1.0f, 27 | stroke = SolidColor(Color(0xFF000000)), 28 | strokeAlpha = 1.0f, 29 | strokeLineWidth = 2f, 30 | strokeLineCap = StrokeCap.Round, 31 | strokeLineJoin = StrokeJoin.Round, 32 | strokeLineMiter = 1.0f, 33 | pathFillType = PathFillType.NonZero 34 | ) { 35 | moveTo(5f, 3f) 36 | horizontalLineTo(19f) 37 | arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 21f, 5f) 38 | verticalLineTo(19f) 39 | arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 19f, 21f) 40 | horizontalLineTo(5f) 41 | arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 3f, 19f) 42 | verticalLineTo(5f) 43 | arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 5f, 3f) 44 | close() 45 | } 46 | path( 47 | fill = null, 48 | fillAlpha = 1.0f, 49 | stroke = SolidColor(Color(0xFF000000)), 50 | strokeAlpha = 1.0f, 51 | strokeLineWidth = 2f, 52 | strokeLineCap = StrokeCap.Round, 53 | strokeLineJoin = StrokeJoin.Round, 54 | strokeLineMiter = 1.0f, 55 | pathFillType = PathFillType.NonZero 56 | ) { 57 | moveTo(3f, 12f) 58 | horizontalLineToRelative(18f) 59 | } 60 | path( 61 | fill = null, 62 | fillAlpha = 1.0f, 63 | stroke = SolidColor(Color(0xFF000000)), 64 | strokeAlpha = 1.0f, 65 | strokeLineWidth = 2f, 66 | strokeLineCap = StrokeCap.Round, 67 | strokeLineJoin = StrokeJoin.Round, 68 | strokeLineMiter = 1.0f, 69 | pathFillType = PathFillType.NonZero 70 | ) { 71 | moveTo(12f, 3f) 72 | verticalLineToRelative(18f) 73 | } 74 | }.build() 75 | return _Grid2x2!! 76 | } 77 | 78 | private var _Grid2x2: ImageVector? = null 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.platform.LocalContext 13 | 14 | private val DarkColorScheme = darkColorScheme( 15 | primary = Purple80, 16 | secondary = PurpleGrey80, 17 | tertiary = Pink80 18 | ) 19 | 20 | private val LightColorScheme = lightColorScheme( 21 | primary = Purple40, 22 | secondary = PurpleGrey40, 23 | tertiary = Pink40 24 | 25 | /* Other default colors to override 26 | background = Color(0xFFFFFBFE), 27 | surface = Color(0xFFFFFBFE), 28 | onPrimary = Color.White, 29 | onSecondary = Color.White, 30 | onTertiary = Color.White, 31 | onBackground = Color(0xFF1C1B1F), 32 | onSurface = Color(0xFF1C1B1F), 33 | */ 34 | ) 35 | 36 | @Composable 37 | fun SharedElementTransitionExampleTheme( 38 | darkTheme: Boolean = isSystemInDarkTheme(), 39 | // Dynamic color is available on Android 12+ 40 | dynamicColor: Boolean = true, 41 | content: @Composable () -> Unit 42 | ) { 43 | val colorScheme = when { 44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 45 | val context = LocalContext.current 46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 47 | } 48 | 49 | darkTheme -> DarkColorScheme 50 | else -> LightColorScheme 51 | } 52 | 53 | MaterialTheme( 54 | colorScheme = colorScheme, 55 | typography = Typography, 56 | content = content 57 | ) 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/binissa/sharedelementtransitionexample/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.binissa.sharedelementtransitionexample.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/bakery_cake_dessert.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/beverage_coffee_cup.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bowl_food.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/burrito.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 40 | 43 | 46 | 49 | 52 | 55 | 58 | 61 | 64 | 67 | 70 | 73 | 76 | 79 | 82 | 85 | 88 | 90 | 91 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 105 | 106 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 121 | 124 | 127 | 130 | 133 | 135 | 136 | 141 | 142 | 143 | 144 | 145 | 146 | 149 | 151 | 152 | 157 | 158 | 159 | 160 | 161 | 162 | 164 | 165 | 170 | 171 | 172 | 173 | 174 | 175 | 177 | 178 | 183 | 184 | 185 | 186 | 187 | 188 | 190 | 191 | 196 | 197 | 198 | 199 | 200 | 201 | 204 | 207 | 208 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/doughnut.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 14 | 19 | 24 | 29 | 34 | 39 | 44 | 49 | 54 | 59 | 64 | 69 | 74 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/french_fries.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 14 | 15 | 16 | 18 | 23 | 24 | 25 | 27 | 32 | 37 | 42 | 47 | 52 | 57 | 62 | 67 | 72 | 77 | 82 | 87 | 92 | 97 | 98 | 99 | 101 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/hamburger.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | 63 | 66 | 69 | 72 | 75 | 78 | 81 | 84 | 87 | 90 | 93 | 96 | 99 | 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/hotdog.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 37 | -------------------------------------------------------------------------------- /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_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sandwich.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/soup.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 59 | 64 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sushi.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Shared Element Transition Example 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |