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