1 /*
<lambda>null2 * Copyright (C) 2025 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 @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
18
19 package com.android.mechanics.demo.util
20
21 import androidx.compose.animation.core.tween
22 import androidx.compose.foundation.clickable
23 import androidx.compose.foundation.layout.Arrangement
24 import androidx.compose.foundation.layout.Row
25 import androidx.compose.foundation.layout.fillMaxWidth
26 import androidx.compose.foundation.layout.padding
27 import androidx.compose.foundation.layout.size
28 import androidx.compose.material.icons.Icons
29 import androidx.compose.material.icons.filled.ExpandMore
30 import androidx.compose.material3.Card
31 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
32 import androidx.compose.material3.Icon
33 import androidx.compose.material3.MaterialTheme
34 import androidx.compose.runtime.Composable
35 import androidx.compose.runtime.getValue
36 import androidx.compose.runtime.remember
37 import androidx.compose.runtime.rememberCoroutineScope
38 import androidx.compose.ui.Modifier
39 import androidx.compose.ui.draw.drawWithContent
40 import androidx.compose.ui.graphics.drawscope.rotate
41 import androidx.compose.ui.unit.dp
42 import com.android.compose.animation.scene.ContentScope
43 import com.android.compose.animation.scene.ElementKey
44 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
45 import com.android.compose.animation.scene.SceneKey
46 import com.android.compose.animation.scene.SceneTransitionLayout
47 import com.android.compose.animation.scene.SceneTransitions
48 import com.android.compose.animation.scene.Swipe
49 import com.android.compose.animation.scene.UserActionDistance
50 import com.android.compose.animation.scene.ValueKey
51 import com.android.compose.animation.scene.animateElementIntAsState
52 import com.android.compose.animation.scene.transitions
53
54 object Scenes {
55 val Collapsed = SceneKey(debugName = "Collapsed")
56 val Expanded = SceneKey(debugName = "Expanded")
57 }
58
59 object Elements {
60 val Card = ElementKey("Card")
61 val Chevron = ElementKey("Chevron")
62 }
63
64 object Values {
65 val ChevronRotation = ValueKey("Rotation")
66 }
67
68 object Transitions {
fromContentnull69 val ExpandedCollapsedDistance = UserActionDistance { fromContent, toContent, orientation ->
70 val expandedSize = Scenes.Expanded.targetSize() ?: return@UserActionDistance 0f
71 val collapsedSize = Scenes.Collapsed.targetSize() ?: return@UserActionDistance 0f
72
73 (expandedSize.height - collapsedSize.height).toFloat()
74 }
<lambda>null75 val DefaultTransition = transitions {
76 from(Scenes.Expanded, Scenes.Collapsed) {
77 spec = tween(500)
78 distance = ExpandedCollapsedDistance
79 }
80 }
81 }
82
83 @Composable
ExpandableCardnull84 fun ExpandableCard(
85 modifier: Modifier = Modifier,
86 transitions: SceneTransitions = remember { Transitions.DefaultTransition },
<lambda>null87 header: @Composable ContentScope.(isExpanded: Boolean) -> Unit = {},
88 content: @Composable ContentScope.(isExpanded: Boolean) -> Unit,
89 ) {
90 val motionScheme = MaterialTheme.motionScheme
91
<lambda>null92 val state = remember {
93 MutableSceneTransitionLayoutState(
94 Scenes.Collapsed,
95 transitions = transitions,
96 motionScheme = motionScheme,
97 )
98 }
99 val coroutineScope = rememberCoroutineScope()
100
<lambda>null101 SceneTransitionLayout(state = state, modifier = modifier) {
102 scene(Scenes.Collapsed, mapOf(Swipe.Down to Scenes.Expanded)) {
103 ExpansionCard(
104 false,
105 onToggleExpanded = { state.setTargetScene(Scenes.Expanded, coroutineScope) },
106 header = header,
107 content = content,
108 )
109 }
110 scene(Scenes.Expanded, mapOf(Swipe.Up to Scenes.Collapsed)) {
111 ExpansionCard(
112 true,
113 onToggleExpanded = { state.setTargetScene(Scenes.Collapsed, coroutineScope) },
114 header = header,
115 content = content,
116 )
117 }
118 }
119 }
120
121 @Composable
ExpansionCardnull122 private fun ContentScope.ExpansionCard(
123 isExpanded: Boolean,
124 onToggleExpanded: () -> Unit,
125 header: @Composable ContentScope.(isExpanded: Boolean) -> Unit,
126 modifier: Modifier = Modifier,
127 content: @Composable ContentScope.(isExpanded: Boolean) -> Unit,
128 ) {
129 Card(modifier = modifier.padding(16.dp).element(Elements.Card)) {
130 Row(
131 horizontalArrangement = Arrangement.SpaceBetween,
132 modifier =
133 Modifier.fillMaxWidth()
134 .clickable(onClick = onToggleExpanded)
135 .padding(start = 16.dp, end = 16.dp, top = 16.dp),
136 ) {
137 header(isExpanded)
138 Chevron(isExpanded)
139 }
140
141 content(isExpanded)
142 }
143 }
144
145 @Composable
ContentScopenull146 private fun ContentScope.Chevron(rotate: Boolean, modifier: Modifier = Modifier) {
147 val key = Elements.Chevron
148 ElementWithValues(key, modifier) {
149 val rotation by animateElementIntAsState(if (rotate) 180 else 0, Values.ChevronRotation)
150
151 content {
152 Icon(
153 Icons.Default.ExpandMore,
154 null,
155 Modifier.size(24.dp).drawWithContent {
156 rotate(rotation.toFloat()) { this@drawWithContent.drawContent() }
157 },
158 )
159 }
160 }
161 }
162