• 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, ExperimentalMaterial3ExpressiveApi::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.fillMaxWidth
28 import androidx.compose.foundation.layout.height
29 import androidx.compose.foundation.layout.offset
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.ExperimentalMaterial3ExpressiveApi
34 import androidx.compose.material3.MaterialTheme
35 import androidx.compose.material3.MotionScheme
36 import androidx.compose.material3.Slider
37 import androidx.compose.material3.Text
38 import androidx.compose.runtime.Composable
39 import androidx.compose.runtime.getValue
40 import androidx.compose.runtime.mutableStateOf
41 import androidx.compose.runtime.remember
42 import androidx.compose.runtime.setValue
43 import androidx.compose.ui.Alignment
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.draw.clip
46 import androidx.compose.ui.draw.drawBehind
47 import androidx.compose.ui.geometry.Offset
48 import androidx.compose.ui.graphics.PathEffect
49 import androidx.compose.ui.layout.onPlaced
50 import androidx.compose.ui.platform.LocalDensity
51 import androidx.compose.ui.unit.IntOffset
52 import androidx.compose.ui.unit.dp
53 import com.android.mechanics.debug.DebugMotionValueVisualization
54 import com.android.mechanics.debug.debugMotionValue
55 import com.android.mechanics.demo.staging.asMechanics
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.spec.Breakpoint
61 import com.android.mechanics.spec.BreakpointKey
62 import com.android.mechanics.spec.DirectionalMotionSpec
63 import com.android.mechanics.spec.InputDirection
64 import com.android.mechanics.spec.Mapping
65 import com.android.mechanics.spec.MotionSpec
66 import com.android.mechanics.spec.OnChangeSegmentHandler
67 import com.android.mechanics.spec.SegmentData
68 import com.android.mechanics.spec.SegmentKey
69 import com.android.mechanics.spec.builder
70 import com.android.mechanics.spec.reverseBuilder
71 import com.android.mechanics.spring.SpringParameters
72 
73 object DirectionSpecDemo : Demo<DirectionSpecDemo.Config> {
74     object Keys {
75         val Start = BreakpointKey("Start")
76         val Detach = BreakpointKey("Detach")
77         val End = Breakpoint.maxLimit.key
78     }
79 
80     data class Config(val defaultSpring: SpringParameters)
81 
82     var inputRange by mutableStateOf(0f..0f)
83 
84     @Composable
85     override fun DemoUi(config: Config, modifier: Modifier) {
86         val colors = MaterialTheme.colorScheme
87 
88         // Also using GestureContext.dragOffset as input.
89         val gestureContext = rememberDistanceGestureContext()
90         val spec = rememberSpec(inputOutputRange = inputRange, config)
91         val motionValue = rememberMotionValue(gestureContext::dragOffset, { spec }, gestureContext)
92 
93         Column(
94             verticalArrangement = Arrangement.spacedBy(24.dp),
95             modifier = modifier.fillMaxWidth().padding(vertical = 24.dp, horizontal = 48.dp),
96         ) {
97             Text("Change Direction Slop")
98 
99             val density = LocalDensity.current
100             Slider(
101                 value = gestureContext.directionChangeSlop,
102                 valueRange = 0.001f..with(density) { 48.dp.toPx() },
103                 onValueChange = { gestureContext.directionChangeSlop = it },
104                 modifier = Modifier.fillMaxWidth(),
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         inputOutputRange: ClosedFloatingPointRange<Float>,
160         config: Config,
161     ): MotionSpec {
162         val delta = inputOutputRange.endInclusive - inputOutputRange.start
163 
164         val startPosPx = inputOutputRange.start
165         val detachPosPx = delta * .4f
166         val attachPosPx = delta * .1f
167 
168         val fastSpring = MotionScheme.expressive().fastSpatialSpec<Float>().asMechanics()
169         val slowSpring = MotionScheme.expressive().slowSpatialSpec<Float>().asMechanics()
170 
171         return remember(inputOutputRange, config) {
172             val detachSpec =
173                 DirectionalMotionSpec.builder(config.defaultSpring, initialMapping = Mapping.Zero)
174                     .toBreakpoint(startPosPx, Keys.Start)
175                     .continueWith(Mapping.Linear(.3f))
176                     .toBreakpoint(detachPosPx, Keys.Detach)
177                     .completeWith(Mapping.Identity, slowSpring)
178 
179             val attachSpec =
180                 DirectionalMotionSpec.reverseBuilder(config.defaultSpring)
181                     .toBreakpoint(attachPosPx, Keys.Detach)
182                     .completeWith(mapping = Mapping.Zero, fastSpring)
183 
184             val segmentHandlers =
185                 mapOf<SegmentKey, OnChangeSegmentHandler>(
186                     SegmentKey(Keys.Detach, Keys.End, InputDirection.Min) to
187                         { currentSegment, _, newDirection ->
188                             if (newDirection != currentSegment.direction) currentSegment else null
189                         },
190                     SegmentKey(Keys.Start, Keys.Detach, InputDirection.Max) to
191                         { currentSegment: SegmentData, newInput: Float, newDirection: InputDirection
192                             ->
193                             if (newDirection != currentSegment.direction && newInput >= 0)
194                                 currentSegment
195                             else null
196                         },
197                 )
198 
199             MotionSpec(
200                 maxDirection = detachSpec,
201                 minDirection = attachSpec,
202                 resetSpring = config.defaultSpring,
203                 segmentHandlers = segmentHandlers,
204             )
205         }
206     }
207 
208     @Composable
209     override fun rememberDefaultConfig(): Config {
210         val defaultSpring = defaultSpatialSpring()
211         return remember(defaultSpring) { Config(defaultSpring) }
212     }
213 
214     override val visualizationInputRange: ClosedFloatingPointRange<Float>
215         get() = inputRange
216 
217     @Composable
218     override fun ColumnScope.ConfigUi(config: Config, onConfigChanged: (Config) -> Unit) {}
219 
220     override val identifier: String = "DirectionSpecDemo"
221 }
222