• 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 package com.android.mechanics.demo.demos
18 
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.foundation.gestures.Orientation
21 import androidx.compose.foundation.gestures.draggable
22 import androidx.compose.foundation.gestures.rememberDraggableState
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.Column
25 import androidx.compose.foundation.layout.ColumnScope
26 import androidx.compose.foundation.layout.fillMaxSize
27 import androidx.compose.foundation.layout.fillMaxWidth
28 import androidx.compose.foundation.layout.offset
29 import androidx.compose.foundation.layout.padding
30 import androidx.compose.foundation.layout.size
31 import androidx.compose.material3.Card
32 import androidx.compose.material3.CardDefaults.outlinedCardColors
33 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
34 import androidx.compose.material3.MaterialTheme
35 import androidx.compose.material3.MotionScheme
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.getValue
38 import androidx.compose.runtime.mutableStateOf
39 import androidx.compose.runtime.remember
40 import androidx.compose.runtime.setValue
41 import androidx.compose.ui.Alignment
42 import androidx.compose.ui.Modifier
43 import androidx.compose.ui.platform.LocalDensity
44 import androidx.compose.ui.unit.Density
45 import androidx.compose.ui.unit.Dp
46 import androidx.compose.ui.unit.IntOffset
47 import androidx.compose.ui.unit.dp
48 import com.android.mechanics.debug.debugMotionValue
49 import com.android.mechanics.demo.staging.asMechanics
50 import com.android.mechanics.demo.staging.defaultSpatialSpring
51 import com.android.mechanics.demo.staging.rememberDistanceGestureContext
52 import com.android.mechanics.demo.staging.rememberMotionValue
53 import com.android.mechanics.demo.tuneable.Demo
54 import com.android.mechanics.demo.tuneable.Section
55 import com.android.mechanics.demo.tuneable.SliderWithPreview
56 import com.android.mechanics.demo.tuneable.SpringParameterSection
57 import com.android.mechanics.spec.BreakpointKey
58 import com.android.mechanics.spec.DirectionalMotionSpec
59 import com.android.mechanics.spec.InputDirection
60 import com.android.mechanics.spec.Mapping
61 import com.android.mechanics.spec.MotionSpec
62 import com.android.mechanics.spec.OnChangeSegmentHandler
63 import com.android.mechanics.spec.SegmentData
64 import com.android.mechanics.spec.SegmentKey
65 import com.android.mechanics.spec.builder
66 import com.android.mechanics.spec.reverseBuilder
67 import com.android.mechanics.spring.SpringParameters
68 import kotlin.math.abs
69 import kotlin.math.roundToInt
70 
71 object MagneticOverviewDismiss : Demo<MagneticOverviewDismiss.Config> {
72     object Keys {
73         val Start = BreakpointKey("Start")
74         val Detach = BreakpointKey("Detach")
75         val Dismiss = BreakpointKey("Dismiss")
76     }
77 
78     data class Config(
79         val defaultSpring: SpringParameters,
80         val snapSpring: SpringParameters,
81         val dismissPosition: Dp = 400.dp,
82         val detachPosition: Dp = 200.dp,
83         val attachPosition: Dp = 50.dp,
84         val startPosition: Dp = 0.dp,
85         val velocityThreshold: Dp = 125.dp,
86         val overdragDistance: Dp = 24.dp,
87     )
88 
89     @Composable
90     override fun DemoUi(config: Config, modifier: Modifier) {
91 
92         val density = LocalDensity.current
93         val spec = remember(config) { with(density) { mutableStateOf(createDetachSpec(config)) } }
94         val gestureContext = rememberDistanceGestureContext()
95         val yOffset = rememberMotionValue(gestureContext::dragOffset, spec::value, gestureContext)
96 
97         Column(modifier = modifier.fillMaxSize().padding(64.dp).debugMotionValue(yOffset)) {
98             Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
99                 Card(
100                     colors = outlinedCardColors(MaterialTheme.colorScheme.primary),
101                     modifier =
102                         modifier
103                             .align(Alignment.Center)
104                             .padding(64.dp)
105                             .size(width = 150.dp, height = 300.dp)
106                             .offset { IntOffset(0, -yOffset.output.toInt()) }
107                             .draggable(
108                                 orientation = Orientation.Vertical,
109                                 state =
110                                     rememberDraggableState { delta ->
111                                         gestureContext.dragOffset -= delta
112                                     },
113                                 onDragStopped = { velocity ->
114                                     with(yOffset.spec.maxDirection) {
115                                         val currentValue = gestureContext.dragOffset
116                                         val detachPosition =
117                                             breakpoints[findBreakpointIndex(Keys.Detach)].position
118                                         val isDismiss =
119                                             if (
120                                                 abs(velocity) >
121                                                     with(density) {
122                                                         config.velocityThreshold.toPx()
123                                                     }
124                                             ) {
125                                                 velocity < 0
126                                             } else {
127                                                 currentValue > detachPosition
128                                             }
129 
130                                         val snapTarget =
131                                             breakpoints[
132                                                     findBreakpointIndex(
133                                                         if (isDismiss) Keys.Dismiss else Keys.Start
134                                                     )]
135                                                 .position
136                                         Animatable(gestureContext.dragOffset).animateTo(
137                                             snapTarget
138                                         ) {
139                                             gestureContext.dragOffset = value
140                                         }
141                                     }
142                                 },
143                             ),
144                 ) {}
145             }
146         }
147     }
148 
149     override var visualizationInputRange by mutableStateOf(0f..1000f)
150 
151     // Stuff below is only demo helpers - configuration, stuff that should go to libraries etc.
152 
153     @Composable
154     override fun ColumnScope.ConfigUi(config: Config, onConfigChanged: (Config) -> Unit) {
155 
156         SpringParameterSection(
157             label = "Snap Spring",
158             value = config.snapSpring,
159             onValueChanged = { onConfigChanged(config.copy(snapSpring = it)) },
160             sectionKey = "snap_spring",
161         )
162 
163         Section(
164             label = "Detach Position",
165             summary = { "$it" },
166             value = config.detachPosition,
167             onValueChanged = { onConfigChanged(config.copy(detachPosition = it)) },
168             sectionKey = "detach_position",
169         ) { value, onValueChanged ->
170             SliderWithPreview(
171                 value = value.value,
172                 valueRange = config.attachPosition.value..config.dismissPosition.value,
173                 onValueChange = { onValueChanged(it.dp) },
174                 render = { "$it" },
175                 normalize = { it.roundToInt().toFloat() },
176                 modifier = Modifier.fillMaxWidth(),
177             )
178         }
179 
180         Section(
181             label = "Attach Position",
182             summary = { "$it" },
183             value = config.attachPosition,
184             onValueChanged = { onConfigChanged(config.copy(attachPosition = it)) },
185             sectionKey = "attach_position",
186         ) { value, onValueChanged ->
187             SliderWithPreview(
188                 value = value.value,
189                 valueRange = config.startPosition.value..config.detachPosition.value,
190                 onValueChange = { onValueChanged(it.dp) },
191                 render = { "$it" },
192                 normalize = { it.roundToInt().toFloat() },
193                 modifier = Modifier.fillMaxWidth(),
194             )
195         }
196     }
197 
198     @Composable
199     @OptIn(ExperimentalMaterial3ExpressiveApi::class)
200     override fun rememberDefaultConfig(): Config {
201         val defaultSpring = defaultSpatialSpring()
202         val snapSpring = MotionScheme.expressive().fastSpatialSpec<Float>().asMechanics()
203         return remember(defaultSpring, snapSpring) { Config(defaultSpring, snapSpring) }
204     }
205 
206     fun Density.createDetachSpec(config: Config): MotionSpec {
207         val overdragDistancePx = config.overdragDistance.toPx()
208         val startPosPx = config.startPosition.toPx()
209         val detachPosPx = config.detachPosition.toPx()
210         val attachPosPx = config.attachPosition.toPx()
211         val dismissPosPx = config.dismissPosition.toPx()
212 
213         val dismissedMapping = Mapping.Fixed(dismissPosPx)
214         val dismissSpec =
215             DirectionalMotionSpec.builder(
216                     config.defaultSpring,
217                     initialMapping = Mapping.Tanh(overdragDistancePx, 3f),
218                 )
219                 .toBreakpoint(startPosPx, Keys.Start)
220                 .continueWith(Mapping.Linear(.3f))
221                 .toBreakpoint(detachPosPx, Keys.Detach)
222                 .continueWith(Mapping.Identity, config.snapSpring)
223                 .toBreakpoint(dismissPosPx, Keys.Dismiss)
224                 .completeWith(dismissedMapping)
225 
226         val abortSpec =
227             DirectionalMotionSpec.reverseBuilder(
228                     config.defaultSpring,
229                     initialMapping = dismissedMapping,
230                 )
231                 .toBreakpoint(dismissPosPx, Keys.Dismiss)
232                 .continueWith(Mapping.Identity)
233                 .toBreakpoint(attachPosPx, Keys.Detach)
234                 .continueWith(mapping = Mapping.Zero, spring = config.snapSpring)
235                 .toBreakpoint(startPosPx, Keys.Start)
236                 .completeWith(Mapping.Tanh(overdragDistancePx, 3f))
237 
238         val segmentHandlers =
239             mapOf<SegmentKey, OnChangeSegmentHandler>(
240                 SegmentKey(Keys.Detach, Keys.Dismiss, InputDirection.Min) to
241                     { currentSegment, _, newDirection ->
242                         if (newDirection != currentSegment.direction) currentSegment else null
243                     },
244                 SegmentKey(Keys.Start, Keys.Detach, InputDirection.Max) to
245                     { currentSegment: SegmentData, newInput: Float, newDirection: InputDirection ->
246                         if (newDirection != currentSegment.direction && newInput >= 0)
247                             currentSegment
248                         else null
249                     },
250             )
251 
252         return MotionSpec(
253             maxDirection = dismissSpec,
254             minDirection = abortSpec,
255             resetSpring = config.defaultSpring,
256             segmentHandlers = segmentHandlers,
257         )
258     }
259 
260     override val identifier: String = "magnetic_dismiss"
261 }
262