• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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