Skip to content

Instantly share code, notes, and snippets.

@sdetilly
Created October 1, 2025 13:06
Show Gist options
  • Select an option

  • Save sdetilly/d56e9e6af602a1d89ed8e5706333c34e to your computer and use it in GitHub Desktop.

Select an option

Save sdetilly/d56e9e6af602a1d89ed8e5706333c34e to your computer and use it in GitHub Desktop.
Shared Element Demo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tillylabs.sharedelementdemo.ui.theme.SharedElementDemoTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SharedElementDemoTheme {
SharedElementDemo()
}
}
}
}
data class DemoItem(
val id: String,
val title: String,
val subtitle: String,
val description: String,
val color: Color,
val icon: ImageVector,
val rating: Float = 4.5f
)
enum class DemoScreen {
Main, Detail
}
@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class)
@Composable
fun SharedElementDemo() {
var currentScreen by remember { mutableStateOf(DemoScreen.Main) }
var selectedItem by remember { mutableStateOf<DemoItem?>(null) }
val demoItems = listOf(
DemoItem("1", "Mountain Adventures", "Outdoor Experience", "Discover breathtaking mountain views and challenging hiking trails perfect for adventure seekers.", Color(0xFF4CAF50), Icons.Default.Star),
DemoItem("2", "Ocean Paradise", "Beach Resort", "Relax on pristine beaches with crystal clear waters and luxurious amenities.", Color(0xFF2196F3), Icons.Default.Favorite),
DemoItem("3", "City Lights", "Urban Exploration", "Experience the vibrant nightlife and cultural richness of metropolitan cities.", Color(0xFF9C27B0), Icons.Default.Person),
DemoItem("4", "Forest Retreat", "Nature Escape", "Immerse yourself in lush forests and reconnect with nature's tranquility.", Color(0xFF795548), Icons.Default.Share),
DemoItem("5", "Desert Oasis", "Adventure Tour", "Journey through vast deserts and discover hidden oases in this unique experience.", Color(0xFFFF9800), Icons.Default.Star),
DemoItem("6", "Arctic Wonder", "Winter Adventure", "Explore the beauty of frozen landscapes and witness the northern lights.", Color(0xFF607D8B), Icons.Default.Favorite)
)
SharedTransitionLayout {
AnimatedContent(
targetState = currentScreen,
transitionSpec = {
fadeIn(animationSpec = tween(300)) + slideInVertically(
animationSpec = tween(300),
initialOffsetY = { if (targetState == DemoScreen.Detail) it else -it }
) togetherWith
fadeOut(animationSpec = tween(300)) + slideOutVertically(
animationSpec = tween(300),
targetOffsetY = { if (targetState == DemoScreen.Detail) -it else it }
)
},
label = "screen_transition"
) { screen ->
when (screen) {
DemoScreen.Main -> MainScreen(
items = demoItems,
onItemClick = { item ->
selectedItem = item
currentScreen = DemoScreen.Detail
},
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
DemoScreen.Detail -> selectedItem?.let { item ->
DetailScreen(
item = item,
onBack = {
currentScreen = DemoScreen.Main
},
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
@Composable
fun MainScreen(
items: List<DemoItem>,
onItemClick: (DemoItem) -> Unit,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"Shared Elements Demo",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
}
) { paddingValues ->
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
items(items) { item ->
with(sharedTransitionScope) {
Card(
modifier = Modifier
.aspectRatio(0.8f)
.clickable { onItemClick(item) }
.sharedElement(
sharedContentState = rememberSharedContentState(key = "card-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 500)
}
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
shape = RoundedCornerShape(16.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(
item.color.copy(alpha = 0.8f),
item.color
)
)
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Box(
modifier = Modifier
.sharedElement(
sharedContentState = rememberSharedContentState(key = "icon-container-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400)
}
)
.size(48.dp)
.background(
Color.White.copy(alpha = 0.2f),
CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = item.icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
Column {
Text(
text = item.title,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "title-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400)
}
)
)
Text(
text = item.subtitle,
fontSize = 12.sp,
color = Color.White.copy(alpha = 0.8f),
modifier = Modifier
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "subtitle-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 450)
}
)
)
}
}
}
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
@Composable
fun DetailScreen(
item: DemoItem,
onBack: () -> Unit,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = Color.White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = item.color
)
)
},
) { paddingValues ->
with(sharedTransitionScope) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp)
) {
item {
Card(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.sharedElement(
sharedContentState = rememberSharedContentState(key = "card-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 500)
}
),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp),
shape = RoundedCornerShape(20.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(
item.color.copy(alpha = 0.7f),
item.color
)
)
)
) {
Box(
modifier = Modifier
.align(Alignment.Center)
.sharedElement(
sharedContentState = rememberSharedContentState(key = "icon-container-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400)
}
)
.size(80.dp)
.background(
Color.White.copy(alpha = 0.2f),
CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = item.icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(40.dp)
)
}
}
}
}
item { Spacer(modifier = Modifier.height(24.dp)) }
item {
Text(
text = item.title,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "title-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400)
}
)
)
}
item { Spacer(modifier = Modifier.height(8.dp)) }
item {
Text(
text = item.subtitle,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "subtitle-${item.id}"),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(durationMillis = 450)
}
)
)
}
item { Spacer(modifier = Modifier.height(16.dp)) }
item {
Row(
verticalAlignment = Alignment.CenterVertically
) {
repeat(5) { index ->
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
tint = if (index < item.rating) Color(0xFFFFC107) else Color.Gray,
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "${item.rating}/5.0",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
)
}
}
item { Spacer(modifier = Modifier.height(24.dp)) }
item {
Text(
text = "Description",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
}
item { Spacer(modifier = Modifier.height(8.dp)) }
item {
Text(
text = item.description,
fontSize = 16.sp,
lineHeight = 24.sp,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f)
)
}
item { Spacer(modifier = Modifier.height(32.dp)) }
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
OutlinedButton(
onClick = { /* No action */ },
modifier = Modifier.weight(1f),
border = BorderStroke(1.dp, item.color)
) {
Text("Learn More", color = item.color)
}
Button(
onClick = { /* No action */ },
modifier = Modifier.weight(1f),
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
containerColor = item.color
)
) {
Text("Book Now", color = Color.White)
}
}
}
item { Spacer(modifier = Modifier.height(100.dp)) }
}
}
}
}
// Theme
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
@Composable
fun SharedElementDemoTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
// Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
// Type
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment