-
-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #43 from skydoves/demo/shared-element-transtion
Implement container transform demo
- Loading branch information
Showing
5 changed files
with
512 additions
and
2 deletions.
There are no files selected for viewing
This file contains 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
365 changes: 365 additions & 0 deletions
365
app/src/main/kotlin/com/skydoves/orbitaldemo/ContainerTransformDemo.kt
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,365 @@ | ||
/* | ||
* Designed and developed by 2023 skydoves (Jaewoong Eum) | ||
* | ||
* 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) | ||
|
||
package com.skydoves.orbitaldemo | ||
|
||
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.spring | ||
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.background | ||
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.fillMaxHeight | ||
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.wrapContentHeight | ||
import androidx.compose.foundation.lazy.grid.GridCells | ||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid | ||
import androidx.compose.foundation.shape.CircleShape | ||
import androidx.compose.foundation.shape.RoundedCornerShape | ||
import androidx.compose.material.Icon | ||
import androidx.compose.material.Surface | ||
import androidx.compose.material.Text | ||
import androidx.compose.material.icons.Icons | ||
import androidx.compose.material.icons.filled.Search | ||
import androidx.compose.material.icons.outlined.Favorite | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.LaunchedEffect | ||
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.blur | ||
import androidx.compose.ui.draw.clip | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.text.font.FontWeight | ||
import androidx.compose.ui.text.style.TextAlign | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.unit.dp | ||
import androidx.compose.ui.unit.sp | ||
import com.skydoves.landscapist.glide.GlideImage | ||
import kotlinx.coroutines.delay | ||
|
||
@Preview | ||
@Composable | ||
fun ContainerTransformDemo(model: MyModel = remember { MyModel().apply { selected = items[1] } }) { | ||
SharedTransitionLayout { | ||
LaunchedEffect(key1 = Unit) { | ||
while (true) { | ||
delay(2500) | ||
if (model.selected == null) { | ||
model.selected = model.items[1] | ||
} else { | ||
model.selected = null | ||
} | ||
} | ||
} | ||
AnimatedContent( | ||
model.selected, | ||
transitionSpec = { | ||
fadeIn(tween(600)) togetherWith | ||
fadeOut(tween(600)) using SizeTransform { _, _ -> | ||
spring() | ||
} | ||
}, | ||
label = "", | ||
) { | ||
if (it != null) { | ||
DetailView(model = model, selected = it, model.items[6]) | ||
} else { | ||
GridView(model = model) | ||
} | ||
} | ||
} | ||
} | ||
|
||
context(SharedTransitionScope) | ||
@Composable | ||
fun Details(kitty: Kitty) { | ||
Column( | ||
Modifier | ||
.padding(start = 10.dp, end = 10.dp, top = 10.dp) | ||
.fillMaxHeight() | ||
.wrapContentHeight(Alignment.Top) | ||
.fillMaxWidth() | ||
.background(Color.White) | ||
.padding(start = 10.dp, end = 10.dp), | ||
) { | ||
Row(verticalAlignment = Alignment.CenterVertically) { | ||
Column { | ||
Spacer(Modifier.size(20.dp)) | ||
Text( | ||
kitty.name, | ||
fontSize = 25.sp, | ||
modifier = Modifier.padding(start = 10.dp), | ||
) | ||
Text( | ||
kitty.breed, | ||
fontSize = 22.sp, | ||
color = Color.Black, | ||
fontWeight = FontWeight.Bold, | ||
modifier = Modifier | ||
.padding(start = 10.dp) | ||
.fillMaxWidth(), | ||
) | ||
Spacer(Modifier.size(10.dp)) | ||
} | ||
Spacer(Modifier.weight(1f)) | ||
Icon( | ||
Icons.Outlined.Favorite, | ||
contentDescription = null, | ||
Modifier | ||
.background(Color(0xffffddee), CircleShape) | ||
.padding(10.dp), | ||
) | ||
Spacer(Modifier.size(10.dp)) | ||
} | ||
Box( | ||
modifier = Modifier | ||
.padding(bottom = 10.dp) | ||
.height(2.dp) | ||
.fillMaxWidth() | ||
.background(Color(0xffeeeeee)), | ||
) | ||
Text( | ||
text = | ||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" + | ||
" mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" + | ||
" eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" + | ||
" enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" + | ||
" congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" + | ||
"\n" + | ||
"Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" + | ||
" eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" + | ||
" natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." + | ||
" Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" + | ||
" volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" + | ||
" placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" + | ||
" gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" + | ||
" odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" + | ||
" ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" + | ||
" mauris at urna dictum ornare.\n" + | ||
"\n" + | ||
"Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" + | ||
" fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" + | ||
" commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" + | ||
" augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." + | ||
" Nullam mattis luctus orci at pulvinar.\n" + | ||
"\n" + | ||
"Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" + | ||
" condimentum, convallis purus at, molestie nisi. In hac habitasse platea" + | ||
" dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" + | ||
" feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" + | ||
" justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" + | ||
" vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" + | ||
" neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." + | ||
" Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." + | ||
" Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" + | ||
" lobortis turpis.\n" + | ||
"\n", | ||
modifier = Modifier.skipToLookaheadSize(), | ||
color = Color.Gray, | ||
fontSize = 15.sp, | ||
) | ||
} | ||
} | ||
|
||
context(AnimatedVisibilityScope, SharedTransitionScope) | ||
@Suppress("UNUSED_PARAMETER") | ||
@Composable | ||
fun DetailView( | ||
model: MyModel, | ||
selected: Kitty, | ||
next: Kitty?, | ||
) { | ||
Column( | ||
Modifier | ||
.sharedBounds( | ||
rememberSharedContentState(key = "container + ${selected.id}"), | ||
this@AnimatedVisibilityScope, | ||
clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp)), | ||
), | ||
) { | ||
Row(Modifier.fillMaxHeight(0.5f)) { | ||
GlideImage( | ||
imageModel = { | ||
"https://raw.githubusercontent.com/PokeAPI/sprites/master/" + | ||
"sprites/pokemon/other/official-artwork/${selected.id + 1}.png" | ||
}, | ||
modifier = Modifier | ||
.padding(10.dp) | ||
.sharedElement( | ||
rememberSharedContentState(key = selected.id), | ||
this@AnimatedVisibilityScope, | ||
placeHolderSize = animatedSize, | ||
) | ||
.fillMaxHeight() | ||
.aspectRatio(1f) | ||
.clip(RoundedCornerShape(20.dp)), | ||
) | ||
if (next != null) { | ||
GlideImage( | ||
imageModel = { | ||
"https://raw.githubusercontent.com/PokeAPI/sprites/master/" + | ||
"sprites/pokemon/other/official-artwork/${next.id + 1}.png" | ||
}, | ||
modifier = Modifier | ||
.padding(top = 10.dp, bottom = 10.dp, end = 10.dp) | ||
.fillMaxWidth() | ||
.fillMaxHeight() | ||
.clip(RoundedCornerShape(20.dp)) | ||
.blur(10.dp), | ||
) | ||
} | ||
} | ||
Details(kitty = selected) | ||
} | ||
} | ||
|
||
context(AnimatedVisibilityScope, SharedTransitionScope) | ||
@Composable | ||
fun GridView(model: MyModel) { | ||
Box(Modifier.background(lessVibrantPurple)) { | ||
Box( | ||
Modifier | ||
.padding(20.dp) | ||
.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f) | ||
.animateEnterExit(fadeIn(), fadeOut()), | ||
) { | ||
SearchBar() | ||
} | ||
LazyVerticalGrid( | ||
columns = GridCells.Fixed(2), | ||
contentPadding = PaddingValues(top = 90.dp), | ||
) { | ||
items(6) { | ||
KittyItem(model.items[it]) | ||
} | ||
} | ||
} | ||
} | ||
|
||
class MyModel { | ||
val items = mutableListOf( | ||
Kitty("Waffle", R.drawable.poster, "Bulbasaur", 0), | ||
Kitty("油条", R.drawable.poster, "ivysaur", 1), | ||
Kitty("Cowboy", R.drawable.poster, "Venusaur", 2), | ||
Kitty("Pepper", R.drawable.poster, "charmander", 3), | ||
Kitty("Unknown", R.drawable.poster, "charmeleon", 4), | ||
Kitty("Unknown", R.drawable.poster, "charizard", 5), | ||
Kitty("YT", R.drawable.poster, "wartortle", 6), | ||
) | ||
var selected: Kitty? by mutableStateOf(null) | ||
} | ||
|
||
context(AnimatedVisibilityScope, SharedTransitionScope) | ||
@Composable | ||
fun KittyItem(kitty: Kitty) { | ||
Column( | ||
Modifier | ||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp) | ||
.sharedBounds( | ||
rememberSharedContentState(key = "container + ${kitty.id}"), | ||
this@AnimatedVisibilityScope, | ||
) | ||
.background(Color.White, RoundedCornerShape(20.dp)), | ||
) { | ||
GlideImage( | ||
imageModel = { | ||
"https://raw.githubusercontent.com/PokeAPI/sprites/" + | ||
"master/sprites/pokemon/other/official-artwork/${kitty.id + 1}.png" | ||
}, | ||
modifier = Modifier | ||
.sharedElement( | ||
rememberSharedContentState(key = kitty.id), | ||
this@AnimatedVisibilityScope, | ||
placeHolderSize = animatedSize, | ||
) | ||
.aspectRatio(1f) | ||
.clip(RoundedCornerShape(20.dp)), | ||
) | ||
Spacer(Modifier.size(10.dp)) | ||
Text( | ||
kitty.name, | ||
fontSize = 18.sp, | ||
modifier = Modifier.padding(start = 10.dp), | ||
) | ||
Spacer(Modifier.size(5.dp)) | ||
Text( | ||
kitty.breed, | ||
fontSize = 15.sp, | ||
color = Color.Black, | ||
fontWeight = FontWeight.Bold, | ||
textAlign = TextAlign.Center, | ||
modifier = Modifier | ||
.padding(start = 10.dp) | ||
.fillMaxWidth(), | ||
) | ||
Spacer(Modifier.size(10.dp)) | ||
} | ||
} | ||
|
||
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 | ||
} | ||
} | ||
|
||
private val lessVibrantPurple = Color(0xfff3edf7) | ||
|
||
@Composable | ||
fun SearchBar() { | ||
Surface( | ||
shape = RoundedCornerShape(40), | ||
modifier = Modifier | ||
.fillMaxWidth() | ||
.height(50.dp), | ||
) { | ||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { | ||
Icon( | ||
imageVector = Icons.Default.Search, | ||
contentDescription = null, | ||
tint = Color.LightGray, | ||
modifier = Modifier.padding(10.dp), | ||
) | ||
Text("Search", color = Color.LightGray, modifier = Modifier.weight(1f)) | ||
Box( | ||
Modifier | ||
.padding(end = 10.dp) | ||
.size(35.dp) | ||
.background(Color(0xffecddff), CircleShape), | ||
) | ||
} | ||
} | ||
} |
Oops, something went wrong.