Skip to content

Instantly share code, notes, and snippets.

@KlassenKonstantin
Last active May 24, 2024 21:32
Show Gist options
  • Save KlassenKonstantin/bd2c805a7deeffe7e417f95450b598d3 to your computer and use it in GitHub Desktop.
Save KlassenKonstantin/bd2c805a7deeffe7e417f95450b598d3 to your computer and use it in GitHub Desktop.
Popup shared transition test
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