Skip to content

Instantly share code, notes, and snippets.

@JunkFood02
Last active April 22, 2024 01:48
Show Gist options
  • Save JunkFood02/bf11191916ce200a9473e96ebfd07ec6 to your computer and use it in GitHub Desktop.
Save JunkFood02/bf11191916ce200a9473e96ebfd07ec6 to your computer and use it in GitHub Desktop.
Container transform for cards (🐈 included)
@file:OptIn(
ExperimentalSharedTransitionApi::class,
ExperimentalMaterial3Api::class
)
package com.example.compose_debug
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.annotation.DrawableRes
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.SizeTransform
import androidx.compose.animation.core.AnimationVector4D
import androidx.compose.animation.core.ExperimentalAnimationSpecApi
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FilledIconToggleButton
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.draw.clip
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
private val ConversationList = listOf(
Conversation(
"视ηͺ—ηŒ«",
"η³»η»Ÿε‡ηΊ§",
"ζˆ‘ζžδΈζ‡‚οΌŒθΏ™δΈͺη³»η»Ÿζ―ζ¬‘ε‡ηΊ§ιƒ½ιœ€θ¦θΏ™δΉˆδΉ…ε—οΌŸ",
lastReplyTime = "10 mins ago",
avatarResId = R.drawable.img
),
Conversation(
"ネコ",
"プレースホルダー",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\nAliquam faucibus purus in massa. Ultricies leo integer malesuada nunc. Vitae nunc sed velit dignissim sodales. Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus.\n\nPellentesque habitant morbi tristique senectus et netus et malesuada fames. Id diam vel quam elementum pulvinar etiam non quam lacus. Nisi lacus sed viverra tellus in.\n\nNibh sed pulvinar proin gravida hendrerit. Lectus proin nibh nisl condimentum id venenatis a. Luctus accumsan tortor posuere ac ut consequat. Nibh cras pulvinar mattis nunc sed blandit libero volutpat sed. Ac ut consequat semper viverra nam libero justo laoreet. Lectus nulla at volutpat diam ut venenatis tellus in. Nunc consequat interdum varius sit amet mattis vulputate enim.",
lastReplyTime = "20 mins ago",
avatarResId = R.drawable.img_4
),
Conversation(
"Algorithm Kitty",
"Leetcode is hard",
"The good news is that LeetCode, like any skill, gets better with focused practice. Here's some advice:\n" +
"\n" +
"Start with the Basics: If you're new, focus on building a strong foundation in data structures and algorithms. Many resources are available for that.\n" +
"Solve Easier Problems First: Start with \"Easy\" problems on LeetCode and gradually work your way up. Celebrate those wins!\n" +
"Don't Just Memorize: Try to understand the underlying concepts behind the solutions. Can you explain it to yourself?\n" +
"Think Out Loud: When stuck, talk through your thought process step by step. Sometimes it helps clarify where your understanding falters.\n" +
"Look at Solutions: Don't be afraid to analyze solutions others have provided after you've given a problem your honest effort. This helps you learn new approaches.",
lastReplyTime = "2 days ago",
avatarResId = R.drawable.img_1,
isFavorite = true
),
)
@Composable
@Preview
fun ConversationContainerTransformDemo() {
var show by remember {
mutableStateOf(false)
}
var conversation: Conversation by remember {
mutableStateOf(ConversationList[1])
}
Surface(color = MaterialTheme.colorScheme.surfaceContainer) {
SharedTransitionLayout {
AnimatedContent(
transitionSpec = {
fadeIn(
tween(
durationMillis = DURATION_ENTER,
delayMillis = DURATION_EXIT_SHORT,
easing = EmphasizedDecelerateEasing
)
) togetherWith fadeOut(
tween(
durationMillis = DURATION_EXIT_SHORT,
easing = EmphasizedAccelerateEasing
)
) using SizeTransform { _, _ ->
tween(durationMillis = DURATION, easing = EmphasizedEasing)
}
},
targetState = show
) {
if (it) {
ConversationPageTransition(
data = conversation,
onBackPressed = { show = !show })
} else {
Scaffold(containerColor = MaterialTheme.colorScheme.surfaceContainer) { values ->
Column(modifier = Modifier.padding(values)) {
CardDemoSearchBar(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = 12.dp)
.padding(vertical = 16.dp)
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 12.dp)
) {
items(ConversationList) { data ->
ConversationCardTransition(
data = data
) {
conversation = data
show = !show
}
}
}
}
}
}
}
}
}
}
context(SharedTransitionScope, AnimatedVisibilityScope)
@OptIn(ExperimentalAnimationSpecApi::class)
@Composable
fun ConversationCardTransition(data: Conversation, onClick: () -> Unit = {}) {
ConversationCard(
data = data,
containerModifier = Modifier.sharedBounds(
boundsTransform = { initial, target ->
tween(durationMillis = DURATION, easing = EmphasizedEasing)
},
enter = fadeIn(
tween(
durationMillis = DURATION_ENTER,
delayMillis = DURATION_EXIT_SHORT,
easing = EmphasizedDecelerateEasing
)
),
exit = fadeOut(
tween(
durationMillis = DURATION_EXIT_SHORT,
easing = EmphasizedAccelerateEasing
)
),
sharedContentState = rememberSharedContentState(key = data),
animatedVisibilityScope = this@AnimatedVisibilityScope,
placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize,
),
imageModifier = Modifier.sharedElement(
// boundsTransform = arcBoundsTransform(durationMillis = DURATION),
boundsTransform = { initial, target ->
tween(durationMillis = DURATION, easing = EmphasizedEasing)
},
state = rememberSharedContentState(key = data.avatarResId),
animatedVisibilityScope = this@AnimatedVisibilityScope,
placeHolderSize = SharedTransitionScope.PlaceHolderSize.contentSize,
),
onClick = onClick
)
}
private val RectToVector: TwoWayConverter<Rect, AnimationVector4D> =
TwoWayConverter(
convertToVector = {
Log.d("Hey", "hey!")
AnimationVector4D(it.center.x, it.center.y, it.height, it.width)
},
convertFromVector = {
Rect(it.v1, it.v2, it.v3, it.v4)
}
)
context(SharedTransitionScope, AnimatedVisibilityScope)
@Composable
fun ConversationPageTransition(data: Conversation, onBackPressed: () -> Unit) {
ConversationPage(
data = data, containerModifier = Modifier.sharedBounds(
boundsTransform = { initial, target ->
tween(durationMillis = DURATION, easing = EmphasizedEasing)
},
enter = fadeIn(
tween(
durationMillis = DURATION_ENTER,
delayMillis = DURATION_EXIT_SHORT,
easing = EmphasizedDecelerateEasing
)
),
exit = fadeOut(
tween(
durationMillis = DURATION_EXIT_SHORT,
easing = EmphasizedAccelerateEasing
)
),
sharedContentState = rememberSharedContentState(key = data),
animatedVisibilityScope = this@AnimatedVisibilityScope,
placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize,
),
imageModifier = Modifier.sharedElement(
boundsTransform = { initial, target ->
tween(durationMillis = DURATION, easing = EmphasizedEasing)
},
state = rememberSharedContentState(key = data.avatarResId),
animatedVisibilityScope = this@AnimatedVisibilityScope,
placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize,
),
navigationIconModifier = Modifier
.renderInSharedTransitionScopeOverlay(zIndexInOverlay = -1f)
.animateEnterExit(
enter = scaleIn(
animationSpec = tween(
durationMillis = DURATION_ENTER,
delayMillis = DURATION_EXIT_SHORT,
easing = EmphasizedDecelerateEasing
)
) + fadeIn(
animationSpec = tween(
DURATION_ENTER,
delayMillis = DURATION_EXIT_SHORT,
easing = EmphasizedDecelerateEasing
)
), exit = scaleOut(
animationSpec = tween(
durationMillis = DURATION_EXIT_SHORT,
delayMillis = 0,
easing = EmphasizedAccelerateEasing
)
) + fadeOut(
animationSpec = tween(
durationMillis = DURATION_EXIT_SHORT,
delayMillis = 0,
easing = EmphasizedAccelerateEasing
)
)
),
onBackPressed = onBackPressed
)
}
@Composable
fun ConversationCard(
containerModifier: Modifier = Modifier,
imageModifier: Modifier = Modifier,
data: Conversation,
onClick: () -> Unit = {}
) {
Card(
modifier = containerModifier,
onClick = onClick,
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Box(modifier = Modifier) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(16.dp))
Image(
painter = painterResource(id = data.avatarResId),
contentDescription = null,
modifier = imageModifier
.clip(CircleShape)
.size(60.dp),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(12.dp))
Column() {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = data.name,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
modifier = Modifier.weight(1f)
)
Text(
text = data.lastReplyTime,
style = MaterialTheme.typography.labelMedium,
maxLines = 1,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(16.dp))
}
Column(modifier = Modifier.padding(end = 48.dp)) {
Text(
text = data.title,
style = MaterialTheme.typography.bodySmall,
maxLines = 1
)
Text(
text = data.content,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
color = MaterialTheme.colorScheme.onSurfaceVariant,
overflow = TextOverflow.Ellipsis
)
}
}
Spacer(modifier = Modifier.width(12.dp))
}
IconToggleButton(
checked = data.isFavorite,
onCheckedChange = {}, modifier = Modifier
.align(Alignment.BottomEnd)
.padding(bottom = 4.dp),
colors = IconButtonDefaults.iconToggleButtonColors(
contentColor = MaterialTheme.colorScheme.outline,
checkedContentColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = if (data.isFavorite) Icons.Outlined.Star else Icons.Outlined.StarBorder,
contentDescription = null,
)
}
}
}
}
@Composable
@Preview
fun ConversationPage(
data: Conversation = ConversationList[1],
containerModifier: Modifier = Modifier,
imageModifier: Modifier = Modifier,
navigationIconModifier: Modifier = Modifier,
onBackPressed: () -> Unit = {}
) {
BackHandler {
onBackPressed()
}
Scaffold(containerColor = MaterialTheme.colorScheme.surfaceContainer, topBar = {
Row(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 24.dp)
.padding(top = 24.dp)
.padding(bottom = 12.dp)
) {
FilledIconButton(
onClick = onBackPressed,
colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest),
modifier = navigationIconModifier
) {
Icon(
imageVector = Icons.Outlined.ArrowBack,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
}
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = data.title, style = MaterialTheme.typography.bodyLarge, maxLines = 1)
Text(
text = "3 Messages",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1
)
}
IconButton(
onClick = { },
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onSurfaceVariant)
) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
}
}
}) {
Column(
modifier = Modifier
.padding(it)
.padding(horizontal = 12.dp)
.padding(top = 4.dp)
) {
Card(
modifier = containerModifier,
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest)
) {
Box(modifier = Modifier) {
Column(
modifier = Modifier
.padding(vertical = 24.dp)
.padding(start = 24.dp, end = 16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = data.avatarResId),
contentDescription = null,
modifier = imageModifier
.clip(CircleShape)
.size(48.dp),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = data.name,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
)
Text(
text = data.lastReplyTime,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
FilledIconToggleButton(
checked = data.isFavorite,
onCheckedChange = {}, modifier = Modifier,
colors = IconButtonDefaults.filledIconToggleButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
contentColor = MaterialTheme.colorScheme.outline,
checkedContentColor = MaterialTheme.colorScheme.primary,
checkedContainerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Icon(
imageVector = if (data.isFavorite) Icons.Outlined.Star else Icons.Outlined.StarBorder,
contentDescription = null,
)
}
}
Column(modifier = Modifier.padding(end = 8.dp)) {
Text(
text = "To Amy, Bob and Cat",
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 20.dp),
style = MaterialTheme.typography.bodySmall
)
Text(
text = data.content,
style = MaterialTheme.typography.bodyMedium,
minLines = 10,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis
)
}
}
}
Row(
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(bottom = 16.dp)
) {
FilledTonalButton(
onClick = { /*TODO*/ },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Text(text = "Reply")
}
Spacer(modifier = Modifier.width(12.dp))
FilledTonalButton(
onClick = { /*TODO*/ },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Text(text = "Reply all")
}
}
}
}
}
}
@Preview
@Composable
private fun ConversationPreview() {
Scaffold(containerColor = MaterialTheme.colorScheme.surfaceContainer) {
Column(modifier = Modifier.padding(it)) {
CardDemoSearchBar(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = 8.dp)
.padding(vertical = 8.dp)
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 8.dp)
) {
items(ConversationList) {
ConversationCard(data = it)
}
}
}
}
}
@Composable
@Preview
private fun CardDemoSearchBar(modifier: Modifier = Modifier) {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceContainerLowest,
modifier = modifier
.fillMaxWidth()
.height(56.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Outlined.Search,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp)
)
Text(
text = "Search replies",
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
Image(
painter = painterResource(id = R.drawable.img_2),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(horizontal = 16.dp)
.size(30.dp)
.clip(CircleShape)
)
}
}
}
@Composable
fun CardList() {
Scaffold { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
CardDemoSearchBar(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = 8.dp)
.padding(vertical = 16.dp)
)
}
}
}
data class Conversation(
val name: String,
val title: String,
val content: String,
val lastReplyTime: String,
@DrawableRes val avatarResId: Int,
val isFavorite: Boolean = false
)
package com.example.compose_debug
import android.view.animation.PathInterpolator
import androidx.compose.animation.core.Easing
// Material 3 Emphasized Easing
// https://m3.material.io/styles/motion/easing-and-duration/tokens-specs
const val DURATION = 600
const val DURATION_ENTER = 400
const val DURATION_ENTER_SHORT = 300
const val DURATION_EXIT = 200
const val DURATION_EXIT_SHORT = 100
private val emphasizedPath = android.graphics.Path().apply {
moveTo(0f, 0f)
cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f)
cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f)
}
val emphasizedDecelerate = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
val emphasizedAccelerate = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
val emphasized = PathInterpolator(emphasizedPath)
val EmphasizedEasing: Easing = Easing { fraction -> emphasized.getInterpolation(fraction) }
val EmphasizedDecelerateEasing =
Easing { fraction -> emphasizedDecelerate.getInterpolation(fraction) }
val EmphasizedAccelerateEasing =
Easing { fraction -> emphasizedAccelerate.getInterpolation(fraction) }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment