Skip to content

Instantly share code, notes, and snippets.

@JunkFood02
Last active April 12, 2024 23:08
Show Gist options
  • Save JunkFood02/fc6a03054009c4d1823f21da4488e3a3 to your computer and use it in GitHub Desktop.
Save JunkFood02/fc6a03054009c4d1823f21da4488e3a3 to your computer and use it in GitHub Desktop.
🐈 A demo showcasing the new shared element transition API in Jetpack Compose!
/*
* Copyright 2024 The Android Open Source Project, JunkFood02
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(
ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class
)
package com.example.compose_debug
import android.view.animation.PathInterpolator
import androidx.activity.compose.BackHandler
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.SharedTransitionScope.PlaceHolderSize.Companion.animatedSize
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.snapping.SnapPosition
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.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.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
import androidx.compose.material3.carousel.rememberCarouselState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
private val CatDrawableList = listOf(R.drawable.img_4, R.drawable.img_3, R.drawable.img_2)
val KittyList = listOf(
Kitty("Windows cat", R.drawable.img, "American short hair", 0),
Kitty("Algorithm cat", R.drawable.img_1, "American short hair", 1),
Kitty("Red–black tree cat", R.drawable.img_2, "American short hair", 2),
Kitty("Windows cat", R.drawable.img, "American short hair", 3),
Kitty("Algorithm cat", R.drawable.img_1, "American short hair", 4),
Kitty("Red–black tree cat", R.drawable.img_2, "American short hair", 5)
)
@Composable
@Preview
fun CarouselContainerTransform() {
var selectedItem1 by remember { mutableIntStateOf(0) }
var selectedItem2 by remember { mutableIntStateOf(0) }
val pagerState1 = remember(selectedItem1) {
PagerState(currentPage = selectedItem1) { KittyList.size }
}
val pagerState2 = remember(selectedItem2) {
PagerState(currentPage = selectedItem2) { KittyList.size }
}
var show: Boolean by remember {
mutableStateOf(false)
}
SharedTransitionLayout {
AnimatedContent(
targetState = show, transitionSpec = {
fadeIn(
tween(
durationMillis = DURATION_ENTER_SHORT, easing = EmphasizedDecelerateEasing
)
) togetherWith fadeOut(
tween(
durationMillis = DURATION_EXIT_SHORT, easing = EmphasizedAccelerateEasing
)
) using SizeTransform { _, _ ->
tween(durationMillis = DURATION, easing = EmphasizedEasing)
}
}, label = "", modifier = Modifier
) {
if (it) {
FullscreenImagePager(pagerState = pagerState1) { page ->
show = !show
selectedItem2 = page
}
} else {
ImagePager(pagerState = pagerState2) { page ->
show = !show
selectedItem1 = page
}
}
}
}
}
context(SharedTransitionScope, AnimatedVisibilityScope)
@Composable
fun ImagePager(pagerState: PagerState, onClick: (Int) -> Unit) {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.statusBarsPadding()
.verticalScroll(rememberScrollState())
) {
SearchBarDemo(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = 8.dp)
.padding(vertical = 16.dp)
)
HorizontalPager(
state = pagerState,
modifier = Modifier,
pageSize = PageSize.Fixed(150.dp),
beyondViewportPageCount = 2,
pageSpacing = 8.dp,
contentPadding = PaddingValues(horizontal = 8.dp),
snapPosition = SnapPosition.Center,
) {
val kitty = KittyList[it]
Box(
modifier = Modifier
) {
Image(painter = painterResource(id = kitty.photoResId),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.sharedElement(
boundsTransform = { _, _ ->
tween(durationMillis = DURATION, easing = EmphasizedEasing)
},
state = rememberSharedContentState(key = kitty.id),
animatedVisibilityScope = this@AnimatedVisibilityScope,
placeHolderSize = animatedSize,
)
.width(150.dp)
.height(200.dp)
.clip(MaterialTheme.shapes.extraLarge)
.clickable { onClick(it) })
Text(
text = kitty.name,
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Medium, shadow = Shadow(
color = Color.Black.copy(alpha = 0.3f),
offset = Offset(2f, 2f),
blurRadius = 10f
)
),
modifier = Modifier
.width(150.dp)
.padding(12.dp)
.align(Alignment.BottomStart)
.renderInSharedTransitionScopeOverlay()
.animateEnterExit(
enter = fadeIn(
tween(
DURATION_EXIT, delayMillis = DURATION_EXIT
)
), exit = fadeOut(tween(100))
),
color = Color.White,
)
}
}
Text(
text = "Smart cat",
modifier = Modifier.padding(top = 16.dp, start = 20.dp, bottom = 4.dp),
style = MaterialTheme.typography.titleSmall
)
val carouselState = rememberCarouselState(initialItem = 0) {
CatDrawableList.size
}
HorizontalMultiBrowseCarousel(
state = carouselState,
preferredItemWidth = 280.dp,
modifier = Modifier
.padding(vertical = 8.dp)
.padding(horizontal = 8.dp)
.height(280.dp),
contentPadding = PaddingValues(horizontal = 0.dp),
itemSpacing = 8.dp
) {
Image(
painter = painterResource(id = CatDrawableList[it]),
contentDescription = null,
modifier = Modifier,
contentScale = ContentScale.Crop
)
}
}
}
}
}
context(SharedTransitionScope, AnimatedVisibilityScope)
@Composable
fun FullscreenImagePager(pagerState: PagerState, onClick: (Int) -> Unit) {
BackHandler {
onClick(pagerState.currentPage)
}
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = Color.Black
) {
Column {
Row(
modifier = Modifier
.statusBarsPadding()
.height(88.dp)
.renderInSharedTransitionScopeOverlay(zIndexInOverlay = -1f)
.animateEnterExit(exit = fadeOut(tween(DURATION_EXIT_SHORT))),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { onClick(pagerState.currentPage) }) {
Icon(
imageVector = Icons.Outlined.ArrowBack,
contentDescription = null,
tint = Color.White
)
}
}
HorizontalPager(
state = pagerState,
modifier = Modifier,
beyondViewportPageCount = 2,
snapPosition = SnapPosition.Center,
) {
Box {
val kitty = KittyList[it]
Image(
painter = painterResource(id = kitty.photoResId),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.padding(horizontal = 8.dp)
.aspectRatio(3f / 4f, true)
.sharedElement(
boundsTransform = { _, _ ->
tween(durationMillis = DURATION, easing = EmphasizedEasing)
},
state = rememberSharedContentState(key = kitty.id),
animatedVisibilityScope = this@AnimatedVisibilityScope,
placeHolderSize = animatedSize,
)
.clip(MaterialTheme.shapes.extraLarge)
)
}
}
}
}
}
}
@Composable
@Preview
fun SearchBarDemo(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 photos",
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Image(
painter = painterResource(id = R.drawable.img_2),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(horizontal = 16.dp)
.size(30.dp)
.clip(CircleShape)
)
}
}
}
// Material 3 Emphasized Easing
// https://m3.material.io/styles/motion/easing-and-duration/tokens-specs
private const val DURATION = 600
private const val DURATION_ENTER = 400
private const val DURATION_ENTER_SHORT = 300
private const val DURATION_EXIT = 200
private 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) }
data class Kitty(val name: String, val photoResId: Int, val breed: String, val id: Int) {
override fun equals(other: Any?): Boolean {
return other is Kitty && other.id == id
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment