├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
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 |
5 |
--------------------------------------------------------------------------------
/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/com/binissa/sharedelementtransitionexample/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.binissa.sharedelementtransitionexample
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.kotlin.compose) apply false
6 | alias(libs.plugins.ksp) apply false
7 | alias(libs.plugins.hilt) apply false
8 | }
--------------------------------------------------------------------------------
/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. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-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 | agp = "8.8.2"
3 | haze = "1.5.0"
4 | kotlin = "2.0.0"
5 | coreKtx = "1.15.0"
6 | junit = "4.13.2"
7 | junitVersion = "1.2.1"
8 | espressoCore = "3.6.1"
9 | lifecycleRuntimeKtx = "2.8.7"
10 | activityCompose = "1.10.1"
11 | composeBom = "2025.02.00"
12 | ksp-version = "2.0.0-1.0.23"
13 | hilt-version = "2.55"
14 | hiltNavigationFragment = "1.2.0"
15 | navigation = "2.8.8"
16 |
17 | [libraries]
18 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
19 | haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
20 | junit = { group = "junit", name = "junit", version.ref = "junit" }
21 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
22 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
23 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
24 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
25 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
26 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
27 | animation = { group = " androidx.compose.animation", name = "animation" }
28 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
29 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
30 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
31 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
32 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
33 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
34 | hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt-version" }
35 | hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt-version" }
36 | androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigation" }
37 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
38 | androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
39 | androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationFragment" }
40 |
41 | [plugins]
42 | android-application = { id = "com.android.application", version.ref = "agp" }
43 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
44 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
45 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp-version" }
46 | hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt-version" }
47 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahmoudElsayedEssa/SharedElementTransition/371878b3bd228f0153fd4d80cf1fca96c9e1ba73/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Mar 08 12:54:19 EET 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
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.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Shared Element Transition Example"
23 | include(":app")
24 |
--------------------------------------------------------------------------------