• 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(ExperimentalAnimatableApi::class)
18 
19 package com.android.mechanics.demo.presentation
20 
21 import android.util.Log
22 import androidx.compose.animation.core.Easing
23 import androidx.compose.animation.core.ExperimentalAnimatableApi
24 import androidx.compose.foundation.background
25 import androidx.compose.foundation.layout.Arrangement
26 import androidx.compose.foundation.layout.Box
27 import androidx.compose.foundation.layout.Column
28 import androidx.compose.foundation.layout.ColumnScope
29 import androidx.compose.foundation.layout.Row
30 import androidx.compose.foundation.layout.fillMaxWidth
31 import androidx.compose.foundation.layout.height
32 import androidx.compose.foundation.layout.offset
33 import androidx.compose.foundation.layout.padding
34 import androidx.compose.foundation.layout.size
35 import androidx.compose.foundation.shape.RoundedCornerShape
36 import androidx.compose.material3.MaterialTheme
37 import androidx.compose.material3.Slider
38 import androidx.compose.material3.Text
39 import androidx.compose.runtime.Composable
40 import androidx.compose.runtime.getValue
41 import androidx.compose.runtime.mutableStateOf
42 import androidx.compose.runtime.remember
43 import androidx.compose.runtime.setValue
44 import androidx.compose.ui.Alignment
45 import androidx.compose.ui.Modifier
46 import androidx.compose.ui.draw.clip
47 import androidx.compose.ui.draw.drawBehind
48 import androidx.compose.ui.geometry.Offset
49 import androidx.compose.ui.graphics.PathEffect
50 import androidx.compose.ui.layout.onPlaced
51 import androidx.compose.ui.unit.IntOffset
52 import androidx.compose.ui.unit.dp
53 import com.android.compose.animation.Easings
54 import com.android.mechanics.debug.DebugMotionValueVisualization
55 import com.android.mechanics.debug.debugMotionValue
56 import com.android.mechanics.demo.staging.defaultSpatialSpring
57 import com.android.mechanics.demo.staging.rememberDistanceGestureContext
58 import com.android.mechanics.demo.staging.rememberMotionValue
59 import com.android.mechanics.demo.tuneable.Demo
60 import com.android.mechanics.demo.tuneable.Dropdown
61 import com.android.mechanics.spec.Mapping
62 import com.android.mechanics.spec.MotionSpec
63 import com.android.mechanics.spec.builder
64 import com.android.mechanics.spring.SpringParameters
65 
66 object SpecDemo : Demo<SpecDemo.Config> {
67     enum class Scenario(val label: String) {
68         Empty("Simple"),
69         Toggle("Toggle"),
70         Steps("Discrete Steps"),
71         TrackNSnap("Track and Snap"),
72         EasingComparison("Easing Comparison"),
73     }
74 
75     data class Config(val defaultSpring: SpringParameters)
76 
77     var inputRange by mutableStateOf(0f..0f)
78 
79     @Composable
80     override fun DemoUi(config: Config, modifier: Modifier) {
81         val colors = MaterialTheme.colorScheme
82         var activeScenario by remember { mutableStateOf(Scenario.Empty) }
83 
84         // Also using GestureContext.dragOffset as input.
85         val gestureContext = rememberDistanceGestureContext()
86         val spec = rememberSpec(activeScenario, inputOutputRange = inputRange, config)
87         val motionValue = rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
88 
89         Column(
90             verticalArrangement = Arrangement.spacedBy(24.dp),
91             modifier = modifier.fillMaxWidth().padding(vertical = 24.dp, horizontal = 48.dp),
92         ) {
93 
94             // Scenario selector
95             Row {
96                 Text("Example Scenario: ")
97 
98                 Dropdown(
99                     activeScenario,
100                     Scenario.entries,
101                     { it.label },
102                     { activeScenario = it },
103                     modifier = Modifier.fillMaxWidth(),
104                 )
105             }
106 
107             // Output visualization
108             val lineColor = colors.primary
109             Box(
110                 contentAlignment = Alignment.CenterStart,
111                 modifier =
112                     Modifier.fillMaxWidth()
113                         .onPlaced { inputRange = 0f..it.size.width.toFloat() }
114                         .drawBehind {
115                             drawLine(
116                                 lineColor,
117                                 start = Offset(x = 0f, y = center.y),
118                                 end = Offset(x = size.width, y = center.y),
119                                 pathEffect =
120                                     PathEffect.dashPathEffect(
121                                         floatArrayOf(4.dp.toPx(), 4.dp.toPx())
122                                     ),
123                             )
124                         },
125             ) {
126                 Box(
127                     modifier =
128                         Modifier.size(24.dp)
129                             .offset {
130                                 val halfSize = 24.dp.toPx() / 2f
131                                 val xOffset = (-halfSize + motionValue.output).toInt()
132                                 IntOffset(x = xOffset, y = 0)
133                             }
134                             .debugMotionValue(motionValue)
135                             .clip(remember { RoundedCornerShape(24.dp) })
136                             .background(colors.primary)
137                 )
138             }
139 
140             // MotionValue visualization
141             DebugMotionValueVisualization(
142                 motionValue,
143                 inputRange,
144                 modifier = Modifier.fillMaxWidth().height(64.dp),
145             )
146 
147             // Input visualization
148             Slider(
149                 value = gestureContext.dragOffset,
150                 valueRange = inputRange,
151                 onValueChange = { gestureContext.dragOffset = it },
152                 modifier = Modifier.fillMaxWidth(),
153             )
154         }
155     }
156 
157     @Composable
158     fun rememberSpec(
159         scenario: Scenario,
160         inputOutputRange: ClosedFloatingPointRange<Float>,
161         config: Config,
162     ): MotionSpec {
163 
164         return remember(scenario, inputOutputRange, config) {
165             when (scenario) {
166                 Scenario.Empty -> MotionSpec.Empty
167                 Scenario.Toggle -> {
168                     MotionSpec.builder(
169                             config.defaultSpring,
170                             initialMapping = Mapping.Fixed(inputOutputRange.start),
171                         )
172                         .toBreakpoint((inputOutputRange.start + inputOutputRange.endInclusive) / 2f)
173                         .completeWith(Mapping.Fixed(inputOutputRange.endInclusive))
174                 }
175 
176                 Scenario.Steps -> {
177                     val steps = 8
178                     val stepSize = (inputOutputRange.start + inputOutputRange.endInclusive) / steps
179 
180                     var underConstruction =
181                         MotionSpec.builder(
182                             config.defaultSpring,
183                             initialMapping = Mapping.Fixed(inputOutputRange.start),
184                         )
185                     repeat(steps - 1) { step ->
186                         underConstruction =
187                             underConstruction
188                                 .toBreakpoint((step + 1) * stepSize)
189                                 .continueWith(Mapping.Fixed((step + 1) * stepSize))
190                     }
191                     underConstruction.complete()
192                 }
193 
194                 Scenario.TrackNSnap -> {
195                     val third = (inputOutputRange.start + inputOutputRange.endInclusive) / 3
196 
197                     MotionSpec.builder(
198                             config.defaultSpring,
199                             initialMapping = Mapping.Fixed(inputOutputRange.start),
200                         )
201                         .toBreakpoint(third)
202                         .jumpTo(third)
203                         .continueWithTargetValue(2 * third)
204                         .toBreakpoint(2 * third)
205                         .completeWith(Mapping.Fixed(inputOutputRange.endInclusive))
206                 }
207 
208                 Scenario.EasingComparison -> {
209                     val dOut = inputOutputRange.start + inputOutputRange.endInclusive
210 
211                     val segmentSizes = buildList {
212                         val fourth = (dOut) / 4
213                         val sixth = (dOut) / 6
214                         repeat(2) {
215                             add(
216                                 (inputOutputRange.start + (it * fourth))..(inputOutputRange.start +
217                                         (it + 1) * fourth)
218                             )
219                         }
220 
221                         repeat(3) {
222                             add(
223                                 (inputOutputRange.start +
224                                     (it * sixth) +
225                                     dOut / 2)..(inputOutputRange.start +
226                                         (it + 1) * sixth +
227                                         dOut / 2)
228                             )
229                         }
230                     }
231 
232                     Log.d("MIKES", "rememberSpec() called $segmentSizes")
233 
234                     fun easingMapping(easing: Easing, range: ClosedFloatingPointRange<Float>) =
235                         Mapping {
236                             val d = range.endInclusive - range.start
237                             easing.transform((it - range.start) / d) * (dOut) +
238                                 inputOutputRange.start
239                         }
240 
241                     MotionSpec.builder(config.defaultSpring, initialMapping = Mapping.Zero)
242                         .toBreakpoint(segmentSizes[0].start)
243                         .continueWith(easingMapping(Easings.Emphasized, segmentSizes[0]))
244                         .toBreakpoint(segmentSizes[1].start)
245                         .continueWith(
246                             easingMapping({ Easings.Emphasized.transform(1 - it) }, segmentSizes[1])
247                         )
248                         .toBreakpoint(segmentSizes[2].start)
249                         .continueWith(Mapping.Fixed(inputOutputRange.start))
250                         .toBreakpoint(segmentSizes[3].start)
251                         .continueWith(Mapping.Fixed(inputOutputRange.endInclusive))
252                         .toBreakpoint(segmentSizes[4].start)
253                         .completeWith(Mapping.Fixed(inputOutputRange.start))
254                 }
255             }
256         }
257     }
258 
259     @Composable
260     override fun rememberDefaultConfig(): Config {
261         val defaultSpring = defaultSpatialSpring()
262         return remember(defaultSpring) { Config(defaultSpring) }
263     }
264 
265     override val visualizationInputRange: ClosedFloatingPointRange<Float>
266         get() = inputRange
267 
268     @Composable
269     override fun ColumnScope.ConfigUi(config: Config, onConfigChanged: (Config) -> Unit) {}
270 
271     override val identifier: String = "SpecDemo"
272 }
273