Instantly share code, notes, and snippets.
Last active
November 14, 2024 17:03
-
Star
10
(10)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save KlassenKonstantin/bd2c805a7deeffe7e417f95450b598d3 to your computer and use it in GitHub Desktop.
Popup shared transition test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val products = listOf( | |
Product("🍎", "Apples"), | |
Product("🍪", "Cookies"), | |
Product("🍉", "Watermelon"), | |
Product("🫐", "Blueberries"), | |
Product("🍊", "Oranges"), | |
Product("🍑", "Peaches"), | |
Product("🥦", "Broccoli"), | |
) | |
// Make the transition a little more poppy | |
private val boundsTransition = BoundsTransform { _, _ -> spring(dampingRatio = Spring.DampingRatioLowBouncy) } | |
class MainActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
enableEdgeToEdge() | |
super.onCreate(savedInstanceState) | |
setContent { | |
TheTheme { | |
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> | |
SharedTransitionLayout { | |
var visibleDetails by remember { mutableStateOf<Product?>(null) } | |
LazyColumn( | |
modifier = Modifier.padding(horizontal = 16.dp), | |
contentPadding = innerPadding, | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
items(products, key = { it.id }) { product -> | |
ProductInList(product = product, visible = visibleDetails != product) { | |
visibleDetails = product | |
} | |
} | |
} | |
ProductInOverlay( | |
product = visibleDetails | |
) { | |
visibleDetails = null | |
} | |
BackHandler(visibleDetails != null) { | |
visibleDetails = null | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
@Composable | |
fun SharedTransitionScope.ProductInList( | |
product: Product, | |
visible: Boolean, | |
modifier: Modifier = Modifier, | |
onClick: () -> Unit | |
) { | |
var height by remember { mutableStateOf<Dp?>(null) } | |
val density = LocalDensity.current | |
Box( | |
modifier = modifier | |
.onSizeChanged { | |
height = density.run { it.height.toDp() } | |
} | |
) { | |
// Since the item disappears after the animation has finished, it would disappear from the list. We want to remember how high it was, to reserve teh space | |
// Not sure how failsafe this is | |
height?.let { | |
Spacer(modifier = Modifier.height(it)) | |
} | |
AnimatedVisibility( | |
visible = visible | |
) { | |
Box( | |
modifier = Modifier | |
.sharedBounds( | |
sharedContentState = rememberSharedContentState(key = "${product.id}_bounds"), | |
animatedVisibilityScope = this, | |
boundsTransform = boundsTransition, | |
) | |
.background(Color.White, RoundedCornerShape(12.dp)) | |
.clip(RoundedCornerShape(12.dp)) | |
) { | |
Item( | |
product = product, | |
modifier = Modifier.sharedElement( | |
state = rememberSharedContentState(key = product.id), | |
animatedVisibilityScope = this@AnimatedVisibility, | |
boundsTransform = boundsTransition, | |
), | |
onClick = onClick | |
) | |
} | |
} | |
} | |
} | |
@Composable | |
fun SharedTransitionScope.ProductInOverlay( | |
product: Product?, | |
modifier: Modifier = Modifier, | |
onDismissRequest: () -> Unit | |
) { | |
AnimatedContent( | |
modifier = modifier, | |
transitionSpec = { | |
// fade the scrim | |
fadeIn() togetherWith fadeOut() | |
}, | |
targetState = product, | |
) { product -> | |
Box( | |
modifier = Modifier.fillMaxSize(), | |
contentAlignment = Alignment.Center | |
) { | |
if (product == null) return@AnimatedContent | |
ScrimOverlay( | |
onDismissRequest = onDismissRequest | |
) | |
Column( | |
modifier = Modifier | |
.padding(horizontal = 16.dp) | |
.sharedBounds( | |
sharedContentState = rememberSharedContentState(key = "${product.id}_bounds"), | |
animatedVisibilityScope = this@AnimatedContent, | |
boundsTransform = boundsTransition, | |
) | |
//.shadow(4.dp, RoundedCornerShape(12.dp)) // TODO Shadow is clipped during transition. I tried to return a null path in clipInOverlayDuringTransition to disable clipping in the overlay altogether, did not work | |
.background(Color.White, RoundedCornerShape(12.dp)) | |
.clip(RoundedCornerShape(12.dp)) | |
) { | |
Item( | |
product = product, | |
modifier = Modifier.sharedElement( | |
state = rememberSharedContentState(key = product.id), | |
animatedVisibilityScope = this@AnimatedContent, | |
boundsTransform = boundsTransition, | |
) | |
) { | |
// Nothing to do | |
} | |
Row( | |
Modifier | |
.fillMaxWidth() | |
.padding(bottom = 8.dp, end = 8.dp), | |
horizontalArrangement = Arrangement.End | |
) { | |
TextButton(onClick = { /*TODO*/ }) { | |
Text(text = "Do things") | |
} | |
} | |
} | |
} | |
} | |
} | |
@Composable | |
fun Item( | |
product: Product, | |
modifier: Modifier = Modifier, | |
onClick: () -> Unit | |
) { | |
Column( | |
modifier = modifier | |
) { | |
Row( | |
modifier = Modifier | |
.fillMaxWidth() | |
.combinedClickable( | |
onLongClick = { | |
onClick() | |
}, | |
onClick = {} | |
), | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
Box( | |
modifier = Modifier | |
.padding(16.dp) | |
.size(48.dp), | |
contentAlignment = Alignment.Center | |
) { | |
Text(text = product.emoji, style = MaterialTheme.typography.titleLarge) | |
} | |
Text(modifier = Modifier.weight(1f), text = product.name, style = MaterialTheme.typography.bodyLarge) | |
Box( | |
modifier = Modifier | |
.padding(end = 16.dp) | |
.background(MaterialTheme.colorScheme.primaryContainer, RoundedCornerShape(4.dp)) | |
) { | |
Text( | |
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), | |
text = "1kg", | |
style = MaterialTheme.typography.labelMedium | |
) | |
} | |
} | |
} | |
} | |
@Composable | |
fun ScrimOverlay( | |
onDismissRequest: () -> Unit | |
) { | |
Box( | |
modifier = Modifier | |
.fillMaxSize() | |
.background(Color.Black.copy(alpha = 0.20f)) | |
.clickable( | |
interactionSource = remember { MutableInteractionSource() }, | |
indication = null, | |
onClick = onDismissRequest | |
), | |
) | |
} | |
data class Product( | |
val emoji: String, | |
val name: String, | |
val id: String = UUID.randomUUID().toString(), | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment