Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.

Commit f9d8922

Browse files
committed
MotionCompose: Shared transformation
Change-Id: I2ac74786640e2e021d87499a3bc59afbc8cdf88c
1 parent 9cbe599 commit f9d8922

File tree

5 files changed

+279
-1
lines changed

5 files changed

+279
-1
lines changed

MotionCompose/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ https://user-images.githubusercontent.com/1237536/142575353-8ed5cc04-34e3-45ea-9
2828

2929
https://user-images.githubusercontent.com/1237536/142538271-480a5573-4231-414a-88cd-9fd9951fc792.mp4
3030

31+
### [Layout > Shared transform](app/src/main/java/com/example/android/compose/motion/demo/sharedtransform)
32+
33+
https://user-images.githubusercontent.com/1237536/144388012-03a82738-40ac-47fb-9bf2-b6730e673c90.mp4
34+
3135
### [Layout > Shared axis](app/src/main/java/com/example/android/compose/motion/demo/sharedaxis)
3236

3337
https://user-images.githubusercontent.com/1237536/143834134-bfb2cebd-2610-4207-b53e-720c9f1b6e62.mp4

MotionCompose/app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ dependencies {
6666
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
6767
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
6868
implementation 'androidx.activity:activity-compose:1.4.0'
69+
implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.0-rc02'
6970

7071
def accompanist_version = '0.21.3-beta'
7172
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"

MotionCompose/app/src/main/java/com/example/android/compose/motion/demo/Demo.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.example.android.compose.motion.demo.fade.FadeDemo
2121
import com.example.android.compose.motion.demo.fadethrough.FadeThroughDemo
2222
import com.example.android.compose.motion.demo.loading.LoadingDemo
2323
import com.example.android.compose.motion.demo.sharedaxis.SharedAxisDemo
24+
import com.example.android.compose.motion.demo.sharedtransform.SharedTransformDemo
2425

2526
enum class Demo(
2627
val title: String,
@@ -55,6 +56,20 @@ enum class Demo(
5556
content = { FadeThroughDemo() }
5657
),
5758

59+
SharedTransform(
60+
title = "Layout > Shared transform",
61+
description = """
62+
Complex layout changes use a shared transformation to create smooth transitions from
63+
one layout to the next. Elements are grouped together and transform as a single unit,
64+
rather than animating independently. This avoids multiple transformations overlapping
65+
and competing for attention.
66+
""".trimIndent().replace('\n', ' '),
67+
apis = listOf(
68+
"updateTransition", "Transition.AnimatedContent"
69+
),
70+
content = { SharedTransformDemo() }
71+
),
72+
5873
SharedAxis(
5974
title = "Layout > Shared axis (Y-axis)",
6075
description = """

MotionCompose/app/src/main/java/com/example/android/compose/motion/demo/fadethrough/FadeThroughDemo.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ private fun DemoCard(
128128
* the motion spec.
129129
*/
130130
@OptIn(ExperimentalAnimationApi::class)
131-
private fun fadeThrough(
131+
fun fadeThrough(
132132
durationMillis: Int = 300
133133
): AnimatedContentScope<Boolean>.() -> ContentTransform {
134134
return {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/*
2+
* Copyright (C) 2021 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.android.compose.motion.demo.sharedtransform
18+
19+
import androidx.compose.animation.AnimatedContent
20+
import androidx.compose.animation.ExperimentalAnimationApi
21+
import androidx.compose.animation.animateColor
22+
import androidx.compose.animation.core.updateTransition
23+
import androidx.compose.foundation.Image
24+
import androidx.compose.foundation.layout.Arrangement
25+
import androidx.compose.foundation.layout.Column
26+
import androidx.compose.foundation.layout.Row
27+
import androidx.compose.foundation.layout.Spacer
28+
import androidx.compose.foundation.layout.aspectRatio
29+
import androidx.compose.foundation.layout.fillMaxWidth
30+
import androidx.compose.foundation.layout.height
31+
import androidx.compose.foundation.layout.padding
32+
import androidx.compose.foundation.layout.size
33+
import androidx.compose.foundation.layout.widthIn
34+
import androidx.compose.foundation.shape.CircleShape
35+
import androidx.compose.foundation.shape.RoundedCornerShape
36+
import androidx.compose.material.Divider
37+
import androidx.compose.material3.MaterialTheme
38+
import androidx.compose.material3.Surface
39+
import androidx.compose.material3.Text
40+
import androidx.compose.material3.TextButton
41+
import androidx.compose.runtime.Composable
42+
import androidx.compose.runtime.getValue
43+
import androidx.compose.runtime.mutableStateOf
44+
import androidx.compose.runtime.saveable.rememberSaveable
45+
import androidx.compose.runtime.setValue
46+
import androidx.compose.ui.Alignment
47+
import androidx.compose.ui.Modifier
48+
import androidx.compose.ui.draw.clip
49+
import androidx.compose.ui.graphics.Color
50+
import androidx.compose.ui.layout.ContentScale
51+
import androidx.compose.ui.res.painterResource
52+
import androidx.compose.ui.text.style.TextAlign
53+
import androidx.compose.ui.text.style.TextOverflow
54+
import androidx.compose.ui.tooling.preview.Preview
55+
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
56+
import androidx.compose.ui.unit.dp
57+
import androidx.constraintlayout.compose.ConstraintLayout
58+
import com.example.android.compose.motion.R
59+
import com.example.android.compose.motion.demo.Demo
60+
import com.example.android.compose.motion.demo.SimpleScaffold
61+
import com.example.android.compose.motion.demo.fadethrough.fadeThrough
62+
import com.example.android.compose.motion.ui.MotionComposeTheme
63+
64+
@Composable
65+
fun SharedTransformDemo() {
66+
SimpleScaffold(title = Demo.SharedTransform.title) {
67+
DemoCard(
68+
modifier = Modifier
69+
.align(Alignment.Center)
70+
.padding(horizontal = 32.dp)
71+
.widthIn(max = 384.dp)
72+
.fillMaxWidth()
73+
)
74+
}
75+
}
76+
77+
@OptIn(ExperimentalAnimationApi::class)
78+
@Composable
79+
private fun DemoCard(
80+
modifier: Modifier = Modifier
81+
) {
82+
Surface(
83+
modifier = modifier,
84+
tonalElevation = 2.dp,
85+
shape = RoundedCornerShape(16.dp),
86+
) {
87+
// The content of this card is laid out by this ConstraintLayout.
88+
ConstraintLayout {
89+
// The card is either expanded or collapsed.
90+
var expanded by rememberSaveable { mutableStateOf(false) }
91+
92+
// The ConstraintLayout has 4 constrained elements. They animate separately during the
93+
// animation, except for the icon that is shared in both the expanded and the
94+
// collapsed states.
95+
val (content, icon, divider, button) = createRefs()
96+
97+
// This transition object coordinates different kinds of animations.
98+
val transition = updateTransition(targetState = expanded, label = "card")
99+
100+
// This is the main content of the card.
101+
// By using the AnimatedContent composable as an extension function of the transition
102+
// object, the animation runs in sync with other animations of the transition.
103+
// The height of this element animates on the state change (SizeTransform), and the
104+
// ConstraintLayout can lay out its children based on the constraints continuously
105+
// during the animation.
106+
transition.AnimatedContent(
107+
// We use the fade-through effect for elements that change between the states.
108+
transitionSpec = fadeThrough(),
109+
modifier = Modifier.constrainAs(content) {
110+
top.linkTo(parent.top, margin = 16.dp)
111+
start.linkTo(parent.start)
112+
end.linkTo(parent.end)
113+
}
114+
) { targetExpanded ->
115+
CardContent(expanded = targetExpanded)
116+
}
117+
118+
// The icon is shared between the expanded and collapsed states.
119+
CardIcon(
120+
modifier = Modifier.constrainAs(icon) {
121+
top.linkTo(parent.top, margin = 16.dp)
122+
end.linkTo(parent.end, margin = 16.dp)
123+
}
124+
)
125+
126+
// The divider becomes transparent in the collapsed state.
127+
val dividerColor by transition.animateColor(label = "divider color") { targetExpanded ->
128+
if (targetExpanded) {
129+
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
130+
} else {
131+
Color.Transparent
132+
}
133+
}
134+
Divider( // TODO: Replace with Material3 Divider when it's available.
135+
modifier = Modifier.constrainAs(divider) {
136+
top.linkTo(content.bottom)
137+
start.linkTo(parent.start)
138+
end.linkTo(parent.end)
139+
},
140+
color = dividerColor
141+
)
142+
143+
// The expand/collapse button is shared between the expanded and collapsed states.
144+
TextButton(
145+
onClick = { expanded = !expanded },
146+
modifier = Modifier.constrainAs(button) {
147+
top.linkTo(divider.bottom, margin = 8.dp)
148+
start.linkTo(parent.start, margin = 8.dp)
149+
// The button is constrained to the bottom of the parent so that it remains
150+
// visible during the animations.
151+
bottom.linkTo(parent.bottom, margin = 8.dp)
152+
}
153+
) {
154+
// The AnimatedContent extension function can be used for any descendant elements,
155+
// not just direct children.
156+
transition.AnimatedContent(transitionSpec = fadeThrough()) { targetExpanded ->
157+
Text(text = if (targetExpanded) "COLLAPSE" else "EXPAND")
158+
}
159+
}
160+
}
161+
}
162+
}
163+
164+
private val CheeseImages = listOf(
165+
R.drawable.cheese_1 to "Cheese 1",
166+
R.drawable.cheese_2 to "Cheese 2",
167+
R.drawable.cheese_3 to "Cheese 3",
168+
R.drawable.cheese_4 to "Cheese 4",
169+
R.drawable.cheese_5 to "Cheese 5"
170+
)
171+
172+
@Composable
173+
private fun CardContent(expanded: Boolean, modifier: Modifier = Modifier) {
174+
Column(modifier = modifier) {
175+
if (expanded) {
176+
ContentTitle(modifier = Modifier.padding(horizontal = 16.dp))
177+
ContentMaker(
178+
modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp)
179+
)
180+
ContentImagesRow(images = CheeseImages.subList(0, 2))
181+
Spacer(modifier = Modifier.height(1.dp))
182+
ContentImagesRow(images = CheeseImages.subList(2, 5))
183+
ContentBody(maxLines = 2, modifier = Modifier.padding(16.dp))
184+
} else {
185+
ContentMaker(modifier = Modifier.padding(horizontal = 16.dp))
186+
ContentTitle(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp))
187+
ContentBody(maxLines = 1, Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp))
188+
ContentImagesRow(images = CheeseImages)
189+
}
190+
}
191+
}
192+
193+
@Composable
194+
private fun ContentTitle(modifier: Modifier = Modifier) {
195+
Text(
196+
text = "Cheeses",
197+
modifier = modifier,
198+
style = MaterialTheme.typography.titleLarge
199+
)
200+
}
201+
202+
@Composable
203+
private fun ContentMaker(modifier: Modifier = Modifier) {
204+
Text(
205+
text = "Maker: Android Cheese",
206+
modifier = modifier,
207+
style = MaterialTheme.typography.bodyMedium,
208+
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
209+
)
210+
}
211+
212+
@Composable
213+
private fun CardIcon(modifier: Modifier = Modifier) {
214+
Image(
215+
painter = painterResource(R.drawable.cheese_1),
216+
contentDescription = null,
217+
modifier = modifier
218+
.size(48.dp)
219+
.clip(CircleShape)
220+
)
221+
}
222+
223+
@Composable
224+
private fun ContentImagesRow(images: List<Pair<Int, String?>>, modifier: Modifier = Modifier) {
225+
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(1.dp)) {
226+
for ((resourceId, contentDescription) in images) {
227+
Image(
228+
painter = painterResource(resourceId),
229+
contentDescription = contentDescription,
230+
contentScale = ContentScale.Crop,
231+
modifier = Modifier
232+
.weight(1f)
233+
.aspectRatio(1f)
234+
)
235+
}
236+
}
237+
}
238+
239+
@Composable
240+
private fun ContentBody(maxLines: Int, modifier: Modifier = Modifier) {
241+
Text(
242+
text = LoremIpsum(32).values.joinToString(" ").replace('\n', ' '),
243+
modifier = modifier,
244+
style = MaterialTheme.typography.bodyMedium,
245+
maxLines = maxLines,
246+
overflow = TextOverflow.Ellipsis,
247+
textAlign = TextAlign.Justify,
248+
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
249+
)
250+
}
251+
252+
@Preview(showBackground = true)
253+
@Composable
254+
private fun PreviewExpandedContent() {
255+
MotionComposeTheme {
256+
SharedTransformDemo()
257+
}
258+
}

0 commit comments

Comments
 (0)