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