• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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(ExperimentalAnimatableApi::class)
18 
19 package com.android.mechanics.demo.presentation
20 
21 import androidx.compose.animation.core.ExperimentalAnimatableApi
22 import androidx.compose.foundation.background
23 import androidx.compose.foundation.layout.Arrangement
24 import androidx.compose.foundation.layout.Box
25 import androidx.compose.foundation.layout.Column
26 import androidx.compose.foundation.layout.ColumnScope
27 import androidx.compose.foundation.layout.Row
28 import androidx.compose.foundation.layout.fillMaxWidth
29 import androidx.compose.foundation.layout.height
30 import androidx.compose.foundation.layout.padding
31 import androidx.compose.foundation.layout.size
32 import androidx.compose.foundation.shape.RoundedCornerShape
33 import androidx.compose.material3.MaterialTheme
34 import androidx.compose.material3.Slider
35 import androidx.compose.material3.Text
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.getValue
38 import androidx.compose.runtime.mutableFloatStateOf
39 import androidx.compose.runtime.mutableStateOf
40 import androidx.compose.runtime.remember
41 import androidx.compose.runtime.setValue
42 import androidx.compose.ui.Alignment
43 import androidx.compose.ui.Modifier
44 import androidx.compose.ui.draw.clip
45 import androidx.compose.ui.draw.drawBehind
46 import androidx.compose.ui.geometry.CornerRadius
47 import androidx.compose.ui.graphics.PathEffect
48 import androidx.compose.ui.graphics.drawscope.Stroke
49 import androidx.compose.ui.graphics.graphicsLayer
50 import androidx.compose.ui.layout.onPlaced
51 import androidx.compose.ui.layout.positionInParent
52 import androidx.compose.ui.platform.LocalDensity
53 import androidx.compose.ui.unit.Dp
54 import androidx.compose.ui.unit.dp
55 import com.android.compose.modifiers.height
56 import com.android.compose.modifiers.width
57 import com.android.mechanics.debug.DebugMotionValueVisualization
58 import com.android.mechanics.demo.staging.defaultSpatialSpring
59 import com.android.mechanics.demo.staging.rememberDistanceGestureContext
60 import com.android.mechanics.demo.staging.rememberMotionValue
61 import com.android.mechanics.demo.tuneable.Demo
62 import com.android.mechanics.demo.tuneable.DpSlider
63 import com.android.mechanics.demo.tuneable.Dropdown
64 import com.android.mechanics.demo.tuneable.SpringParameterSection
65 import com.android.mechanics.spec.Guarantee
66 import com.android.mechanics.spec.Mapping
67 import com.android.mechanics.spec.MotionSpec
68 import com.android.mechanics.spec.builder
69 import com.android.mechanics.spring.SpringParameters
70 import kotlin.math.min
71 
72 object GuaranteeBoxDemo : Demo<GuaranteeBoxDemo.Config> {
73     enum class Scenario(val label: String) {
74         Mapped("Mapped"),
75         Triggered("With Triggers"),
76         Guaranteed("With Gurarantee"),
77     }
78 
79     data class Config(
80         val defaultSpring: SpringParameters,
81         val boxWidth: Dp,
82         val minVisibleWidth: Dp,
83         val guaranteeDistance: Dp,
84     )
85 
86     var inputRange by mutableStateOf(0f..0f)
87 
88     @Composable
DemoUinull89     override fun DemoUi(config: Config, modifier: Modifier) {
90         val colors = MaterialTheme.colorScheme
91         var activeScenario by remember { mutableStateOf(Scenario.Guaranteed) }
92 
93         var placedBoxWidth by remember { mutableFloatStateOf(0f) }
94         var placedBoxX by remember { mutableFloatStateOf(0f) }
95 
96         // Also using GestureContext.dragOffset as input.
97         val gestureContext = rememberDistanceGestureContext()
98         val spec =
99             rememberSpec(
100                 activeScenario,
101                 { placedBoxX },
102                 { placedBoxWidth },
103                 inputOutputRange = inputRange,
104                 config,
105             )
106         val motionValue = rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
107         Column(
108             verticalArrangement = Arrangement.spacedBy(24.dp),
109             modifier = modifier.fillMaxWidth().padding(vertical = 24.dp, horizontal = 48.dp),
110         ) {
111 
112             // Scenario selector
113             Row {
114                 Text("Example Scenario: ")
115 
116                 Dropdown(
117                     activeScenario,
118                     Scenario.entries,
119                     { it.label },
120                     { activeScenario = it },
121                     modifier = Modifier.fillMaxWidth(),
122                 )
123             }
124 
125             // Output visualization
126             val density = LocalDensity.current
127             var minShapeSize by remember { mutableFloatStateOf(with(density) { 16.dp.toPx() }) }
128 
129             Box(
130                 contentAlignment = Alignment.Center,
131                 modifier =
132                     Modifier.fillMaxWidth().padding(horizontal = 48.dp).onPlaced {
133                         inputRange = 0f..it.size.width.toFloat()
134                     },
135             ) {
136                 val colors = MaterialTheme.colorScheme
137                 val stroke =
138                     remember(this) {
139                         with(density) {
140                             val intervals = floatArrayOf(4.dp.toPx(), 4.dp.toPx())
141                             Stroke(2.dp.toPx(), pathEffect = PathEffect.dashPathEffect(intervals))
142                         }
143                     }
144 
145                 // To illustrate the effect, use an outer Box to visually hint the spec dimension.
146                 // In real code, the dimensions for the spec are derived from Lookahead instead
147                 Box(
148                     contentAlignment = Alignment.CenterStart,
149                     modifier =
150                         Modifier.size(config.boxWidth, 64.dp)
151                             .onPlaced {
152                                 placedBoxWidth = it.size.width.toFloat()
153                                 placedBoxX = it.positionInParent().x
154                             }
155                             .drawBehind {
156                                 drawRoundRect(
157                                     colors.primary,
158                                     cornerRadius = CornerRadius(24.dp.toPx()),
159                                     alpha = 0.1f,
160                                     style = stroke,
161                                 )
162                             },
163                 ) {
164                     Box(
165                         modifier =
166                             Modifier.width { motionValue.floatValue.toInt() }
167                                 .height {
168                                     // Make it look prettier by offsetting the height from
169                                     val targetHeight = 64.dp.toPx()
170                                     val cornerRadius = 24.dp.toPx() * 2
171 
172                                     (targetHeight -
173                                             (cornerRadius - motionValue.floatValue).coerceAtLeast(
174                                                 0f
175                                             ))
176                                         .toInt()
177                                 }
178                                 .clip(remember { RoundedCornerShape(24.dp) })
179                                 .graphicsLayer {
180                                     alpha =
181                                         (motionValue.floatValue / (minShapeSize / 2)).coerceIn(
182                                             0f,
183                                             1f,
184                                         )
185                                 }
186                                 .background(MaterialTheme.colorScheme.primary)
187                     )
188                 }
189             }
190             // MotionValue visualization
191             DebugMotionValueVisualization(
192                 motionValue,
193                 inputRange,
194                 modifier = Modifier.fillMaxWidth().height(64.dp),
195             )
196 
197             // Input visualization
198             Slider(
199                 value = gestureContext.dragOffset,
200                 valueRange = inputRange,
201                 onValueChange = { gestureContext.dragOffset = it },
202                 modifier = Modifier.fillMaxWidth(),
203             )
204         }
205     }
206 
207     @Composable
rememberSpecnull208     fun rememberSpec(
209         scenario: Scenario,
210         x: () -> Float,
211         width: () -> Float,
212         inputOutputRange: ClosedFloatingPointRange<Float>,
213         config: Config,
214     ): MotionSpec {
215 
216         val density = LocalDensity.current
217         val left = x()
218         val widthVal = width()
219         val right = left + widthVal
220 
221         return remember(scenario, inputOutputRange, config, left, widthVal, density) {
222             with(density) {
223                 val guarantee = Guarantee.InputDelta(config.guaranteeDistance.toPx())
224                 val minSize = config.minVisibleWidth.toPx()
225                 when (scenario) {
226                     Scenario.Mapped ->
227                         MotionSpec.builder(config.defaultSpring, initialMapping = Mapping.Zero)
228                             .toBreakpoint(left)
229                             .jumpTo(0f)
230                             .continueWithTargetValue(widthVal)
231                             .toBreakpoint(right)
232                             .completeWith(Mapping.Fixed(widthVal))
233 
234                     Scenario.Triggered ->
235                         MotionSpec.builder(config.defaultSpring, initialMapping = Mapping.Zero)
236                             .toBreakpoint(min(left + minSize, right))
237                             .jumpTo(minSize)
238                             .continueWithTargetValue(widthVal - minSize)
239                             .toBreakpoint(right)
240                             .completeWith(Mapping.Fixed(widthVal))
241 
242                     Scenario.Guaranteed ->
243                         MotionSpec.builder(config.defaultSpring, initialMapping = Mapping.Zero)
244                             .toBreakpoint(min(left + minSize, right))
245                             .jumpTo(minSize, guarantee = guarantee)
246                             .continueWithTargetValue(widthVal - minSize)
247                             .toBreakpoint(right)
248                             .completeWith(Mapping.Fixed(widthVal), guarantee = guarantee)
249                 }
250             }
251         }
252     }
253 
254     @Composable
rememberDefaultConfignull255     override fun rememberDefaultConfig(): Config {
256         val defaultSpring = defaultSpatialSpring()
257         return remember(defaultSpring) {
258             Config(
259                 defaultSpring,
260                 boxWidth = 192.dp,
261                 minVisibleWidth = 24.dp,
262                 guaranteeDistance = 24.dp,
263             )
264         }
265     }
266 
267     override val visualizationInputRange: ClosedFloatingPointRange<Float>
268         get() = inputRange
269 
270     @Composable
ConfigUinull271     override fun ColumnScope.ConfigUi(config: Config, onConfigChanged: (Config) -> Unit) {
272 
273         SpringParameterSection(
274             "spring",
275             config.defaultSpring,
276             { onConfigChanged(config.copy(defaultSpring = it)) },
277             "spring",
278             modifier = Modifier.fillMaxWidth(),
279         )
280 
281         Text("Box Width")
282         DpSlider(
283             config.boxWidth,
284             { onConfigChanged(config.copy(boxWidth = it)) },
285             valueRange = 48.dp..200.dp,
286             modifier = Modifier.fillMaxWidth(),
287         )
288 
289         Text("Min visible width")
290         DpSlider(
291             config.minVisibleWidth,
292             { onConfigChanged(config.copy(minVisibleWidth = it)) },
293             valueRange = 0.dp..48.dp,
294             modifier = Modifier.fillMaxWidth(),
295         )
296 
297         Text("Guarantee Distance")
298         DpSlider(
299             config.guaranteeDistance,
300             { onConfigChanged(config.copy(guaranteeDistance = it)) },
301             valueRange = 0.dp..200.dp,
302             modifier = Modifier.fillMaxWidth(),
303         )
304     }
305 
306     override val identifier: String = "GuaranteeBoxDemo"
307 }
308