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

Commit 9cbe599

Browse files
committed
MotionCompose: Shared axis
Change-Id: I14aaf2f0671039857ddeacd71abae89de409e3f6
1 parent b18a4b0 commit 9cbe599

File tree

3 files changed

+310
-0
lines changed

3 files changed

+310
-0
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 axis](app/src/main/java/com/example/android/compose/motion/demo/sharedaxis)
32+
33+
https://user-images.githubusercontent.com/1237536/143834134-bfb2cebd-2610-4207-b53e-720c9f1b6e62.mp4
34+
3135
### [List > Loading](app/src/main/java/com/example/android/compose/motion/demo/loading)
3236

3337
https://user-images.githubusercontent.com/1237536/143526001-7621d2db-1228-4011-ae84-1572909e7806.mp4

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
2020
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
23+
import com.example.android.compose.motion.demo.sharedaxis.SharedAxisDemo
2324

2425
enum class Demo(
2526
val title: String,
@@ -54,6 +55,19 @@ enum class Demo(
5455
content = { FadeThroughDemo() }
5556
),
5657

58+
SharedAxis(
59+
title = "Layout > Shared axis (Y-axis)",
60+
description = """
61+
The shared axis pattern is used for transitions between UI elements that have a spatial
62+
or navigational relationship. This demo uses a shared transformation on the Y-axis to
63+
reinforce the sequential order of elements.
64+
""".trimIndent().replace('\n', ' '),
65+
apis = listOf(
66+
"AnimatedContent"
67+
),
68+
content = { SharedAxisDemo() }
69+
),
70+
5771
Loading(
5872
title = "List > Loading",
5973
description = """
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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.sharedaxis
18+
19+
import androidx.annotation.DrawableRes
20+
import androidx.compose.animation.AnimatedContent
21+
import androidx.compose.animation.AnimatedVisibilityScope
22+
import androidx.compose.animation.ContentTransform
23+
import androidx.compose.animation.ExperimentalAnimationApi
24+
import androidx.compose.animation.animateColor
25+
import androidx.compose.animation.core.FastOutLinearInEasing
26+
import androidx.compose.animation.core.FiniteAnimationSpec
27+
import androidx.compose.animation.core.LinearOutSlowInEasing
28+
import androidx.compose.animation.core.tween
29+
import androidx.compose.animation.core.updateTransition
30+
import androidx.compose.animation.fadeIn
31+
import androidx.compose.animation.fadeOut
32+
import androidx.compose.animation.slideInVertically
33+
import androidx.compose.animation.slideOutVertically
34+
import androidx.compose.foundation.Image
35+
import androidx.compose.foundation.background
36+
import androidx.compose.foundation.layout.Arrangement
37+
import androidx.compose.foundation.layout.Column
38+
import androidx.compose.foundation.layout.Row
39+
import androidx.compose.foundation.layout.aspectRatio
40+
import androidx.compose.foundation.layout.fillMaxWidth
41+
import androidx.compose.foundation.layout.padding
42+
import androidx.compose.foundation.layout.size
43+
import androidx.compose.foundation.layout.wrapContentSize
44+
import androidx.compose.foundation.rememberScrollState
45+
import androidx.compose.foundation.shape.CircleShape
46+
import androidx.compose.foundation.shape.RoundedCornerShape
47+
import androidx.compose.foundation.verticalScroll
48+
import androidx.compose.material3.IconButton
49+
import androidx.compose.material3.MaterialTheme
50+
import androidx.compose.material3.Text
51+
import androidx.compose.runtime.Composable
52+
import androidx.compose.runtime.getValue
53+
import androidx.compose.runtime.mutableStateOf
54+
import androidx.compose.runtime.remember
55+
import androidx.compose.runtime.saveable.rememberSaveable
56+
import androidx.compose.runtime.setValue
57+
import androidx.compose.ui.Alignment
58+
import androidx.compose.ui.Modifier
59+
import androidx.compose.ui.draw.clip
60+
import androidx.compose.ui.layout.ContentScale
61+
import androidx.compose.ui.platform.LocalDensity
62+
import androidx.compose.ui.res.painterResource
63+
import androidx.compose.ui.text.font.FontWeight
64+
import androidx.compose.ui.text.style.TextAlign
65+
import androidx.compose.ui.tooling.preview.Preview
66+
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
67+
import androidx.compose.ui.unit.dp
68+
import androidx.compose.ui.unit.sp
69+
import com.example.android.compose.motion.demo.CheeseImages
70+
import com.example.android.compose.motion.demo.CheeseNames
71+
import com.example.android.compose.motion.demo.Demo
72+
import com.example.android.compose.motion.demo.SimpleScaffold
73+
import com.example.android.compose.motion.ui.MotionComposeTheme
74+
import com.google.accompanist.insets.LocalWindowInsets
75+
import com.google.accompanist.insets.rememberInsetsPaddingValues
76+
77+
@Composable
78+
fun SharedAxisDemo() {
79+
SimpleScaffold(title = Demo.SharedAxis.title) {
80+
val pages = remember { createPages() }
81+
// Indicator column
82+
var id by rememberSaveable { mutableStateOf(1) }
83+
Row(modifier = Modifier.padding(end = 16.dp)) {
84+
PageIndicatorsColumn(
85+
pages = pages,
86+
selectedId = id,
87+
onIndicatorClick = { id = it }
88+
)
89+
90+
// SharedYAxis animates the content change.
91+
SharedYAxis(targetState = pages.first { it.id == id }) { page ->
92+
PageContent(page = page)
93+
}
94+
}
95+
}
96+
}
97+
98+
/**
99+
* Animates content change with the vertical shared axis pattern.
100+
*
101+
* See [Shared axis](https://material.io/design/motion/the-motion-system.html#shared-axis) for the
102+
* detail about this motion pattern.
103+
*/
104+
@OptIn(ExperimentalAnimationApi::class)
105+
@Composable
106+
private fun <T : Comparable<T>> SharedYAxis(
107+
targetState: T,
108+
modifier: Modifier = Modifier,
109+
content: @Composable AnimatedVisibilityScope.(T) -> Unit
110+
) {
111+
val exitDurationMillis = 80
112+
val enterDurationMillis = 220
113+
114+
// This local function creates the AnimationSpec for outgoing elements.
115+
fun <T> exitSpec(): FiniteAnimationSpec<T> =
116+
tween(
117+
durationMillis = exitDurationMillis,
118+
easing = FastOutLinearInEasing
119+
)
120+
121+
// This local function creates the AnimationSpec for incoming elements.
122+
fun <T> enterSpec(): FiniteAnimationSpec<T> =
123+
tween(
124+
// The enter animation runs right after the exit animation.
125+
delayMillis = exitDurationMillis,
126+
durationMillis = enterDurationMillis,
127+
easing = LinearOutSlowInEasing
128+
)
129+
130+
val slideDistance = with(LocalDensity.current) { 30.dp.roundToPx() }
131+
132+
AnimatedContent(
133+
targetState = targetState,
134+
transitionSpec = {
135+
// The state type (<T>) is Comparable.
136+
// We compare the initial state and the target state to determine whether we are moving
137+
// down or up.
138+
if (initialState < targetState) { // Move down
139+
ContentTransform(
140+
// Outgoing elements fade out and slide up to the top.
141+
initialContentExit = fadeOut(exitSpec()) +
142+
slideOutVertically(exitSpec()) { -slideDistance },
143+
// Incoming elements fade in and slide up from the bottom.
144+
targetContentEnter = fadeIn(enterSpec()) +
145+
slideInVertically(enterSpec()) { slideDistance }
146+
)
147+
} else { // Move up
148+
ContentTransform(
149+
// Outgoing elements fade out and slide down to the bottom.
150+
initialContentExit = fadeOut(exitSpec()) +
151+
slideOutVertically(exitSpec()) { slideDistance },
152+
// Outgoing elements fade in and slide down from the top.
153+
targetContentEnter = fadeIn(enterSpec()) +
154+
slideInVertically(enterSpec()) { -slideDistance }
155+
)
156+
}
157+
},
158+
modifier = modifier,
159+
content = content
160+
)
161+
}
162+
163+
private class Page(
164+
val id: Int,
165+
@DrawableRes
166+
val image: Int,
167+
val title: String,
168+
val body: String
169+
) : Comparable<Page> {
170+
171+
override fun compareTo(other: Page): Int {
172+
return id.compareTo(other.id)
173+
}
174+
}
175+
176+
private fun createPages(): List<Page> {
177+
val body = LoremIpsum().values.joinToString(separator = " ").replace('\n', ' ')
178+
return (0..4).map { i ->
179+
Page(
180+
id = i + 1,
181+
image = CheeseImages[i % CheeseImages.size],
182+
title = CheeseNames[i * 128],
183+
body = body
184+
)
185+
}
186+
}
187+
188+
@Composable
189+
private fun PageIndicatorsColumn(
190+
pages: List<Page>,
191+
selectedId: Int,
192+
onIndicatorClick: (index: Int) -> Unit,
193+
modifier: Modifier = Modifier
194+
) {
195+
Column(
196+
modifier = modifier
197+
.verticalScroll(rememberScrollState())
198+
.padding(16.dp),
199+
horizontalAlignment = Alignment.CenterHorizontally,
200+
verticalArrangement = Arrangement.spacedBy(32.dp),
201+
) {
202+
for (page in pages) {
203+
PageIndicator(
204+
index = page.id,
205+
selected = selectedId == page.id,
206+
onClick = { onIndicatorClick(page.id) }
207+
)
208+
}
209+
}
210+
}
211+
212+
@Composable
213+
private fun PageIndicator(
214+
index: Int,
215+
selected: Boolean,
216+
onClick: () -> Unit
217+
) {
218+
val transition = updateTransition(targetState = selected, label = "indicator")
219+
val backgroundColor by transition.animateColor(label = "background color") { targetSelected ->
220+
if (targetSelected) {
221+
MaterialTheme.colorScheme.primary
222+
} else {
223+
MaterialTheme.colorScheme.surfaceVariant
224+
}
225+
}
226+
val textColor by transition.animateColor(label = "text color") { targetSelected ->
227+
if (targetSelected) {
228+
MaterialTheme.colorScheme.onPrimary
229+
} else {
230+
MaterialTheme.colorScheme.onSurfaceVariant
231+
}
232+
}
233+
IconButton(onClick = onClick) {
234+
Text(
235+
text = index.toString(),
236+
modifier = Modifier
237+
.size(32.dp)
238+
.background(backgroundColor, CircleShape)
239+
.wrapContentSize(),
240+
color = textColor,
241+
fontSize = 20.sp,
242+
fontWeight = FontWeight.Bold
243+
)
244+
}
245+
}
246+
247+
@Composable
248+
private fun PageContent(
249+
page: Page,
250+
modifier: Modifier = Modifier
251+
) {
252+
val systemBars = LocalWindowInsets.current.systemBars
253+
Column(
254+
modifier = modifier
255+
.verticalScroll(rememberScrollState())
256+
.padding(
257+
rememberInsetsPaddingValues(
258+
insets = systemBars,
259+
applyStart = false,
260+
applyTop = false,
261+
additionalBottom = 16.dp
262+
)
263+
),
264+
verticalArrangement = Arrangement.spacedBy(16.dp)
265+
) {
266+
Image(
267+
painter = painterResource(page.image),
268+
contentDescription = null,
269+
modifier = Modifier
270+
.fillMaxWidth()
271+
.aspectRatio(16f / 9f)
272+
.clip(RoundedCornerShape(16.dp)),
273+
contentScale = ContentScale.Crop
274+
)
275+
Text(
276+
text = page.title,
277+
style = MaterialTheme.typography.titleLarge
278+
)
279+
Text(
280+
text = page.body,
281+
textAlign = TextAlign.Justify
282+
)
283+
}
284+
}
285+
286+
@Preview(showBackground = true)
287+
@Composable
288+
private fun PreviewSharedAxisDemo() {
289+
MotionComposeTheme {
290+
SharedAxisDemo()
291+
}
292+
}

0 commit comments

Comments
 (0)