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