1 /* 2 * Copyright (C) 2024 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 18 19 import androidx.compose.runtime.Stable 20 import androidx.compose.runtime.getValue 21 import androidx.compose.runtime.mutableFloatStateOf 22 import androidx.compose.runtime.mutableStateOf 23 import androidx.compose.runtime.setValue 24 import com.android.mechanics.spec.InputDirection 25 import kotlin.math.max 26 import kotlin.math.min 27 28 /** 29 * Gesture-specific context to augment [MotionValue.currentInput]. 30 * 31 * This context helps to capture the user's intent, and should be provided to [MotionValue]s that 32 * respond to a user gesture. 33 */ 34 @Stable 35 interface GestureContext { 36 37 /** 38 * The intrinsic direction of the [MotionValue.currentInput]. 39 * 40 * This property determines which of the [DirectionalMotionSpec] from the [MotionSpec] is used, 41 * and also prevents flip-flopping of the output value on tiny input-changes around a 42 * breakpoint. 43 * 44 * If the [MotionValue.currentInput] is driven - directly or indirectly - by a user gesture, 45 * this property should only change direction after the gesture travelled a significant distance 46 * in the opposite direction. 47 * 48 * @see DistanceGestureContext for a default implementation. 49 */ 50 val direction: InputDirection 51 52 /** 53 * The gesture distance of the current gesture, in pixels. 54 * 55 * Used solely for the [GestureDragDelta] [Guarantee]. Can be hard-coded to a static value if 56 * this type of [Guarantee] is not used. 57 */ 58 val dragOffset: Float 59 } 60 61 /** 62 * [GestureContext] with a mutable [dragOffset]. 63 * 64 * The implementation class defines whether the [direction] is updated accordingly. 65 */ 66 interface MutableDragOffsetGestureContext : GestureContext { 67 /** The gesture distance of the current gesture, in pixels. */ 68 override var dragOffset: Float 69 } 70 71 /** [GestureContext] implementation for manually set values. */ 72 class ProvidedGestureContext(dragOffset: Float, direction: InputDirection) : 73 MutableDragOffsetGestureContext { 74 override var direction by mutableStateOf(direction) 75 override var dragOffset by mutableFloatStateOf(dragOffset) 76 } 77 78 /** 79 * [GestureContext] driven by a gesture distance. 80 * 81 * The direction is determined from the gesture input, where going further than 82 * [directionChangeSlop] in the opposite direction toggles the direction. 83 * 84 * @param initialDragOffset The initial [dragOffset] of the [GestureContext] 85 * @param initialDirection The initial [direction] of the [GestureContext] 86 * @param directionChangeSlop the amount [dragOffset] must be moved in the opposite direction for 87 * the [direction] to flip. 88 */ 89 class DistanceGestureContext( 90 initialDragOffset: Float, 91 initialDirection: InputDirection, 92 directionChangeSlop: Float, 93 ) : MutableDragOffsetGestureContext { 94 init { <lambda>null95 require(directionChangeSlop > 0) { 96 "directionChangeSlop must be greater than 0, was $directionChangeSlop" 97 } 98 } 99 100 override var direction by mutableStateOf(initialDirection) 101 private set 102 103 private var furthestDragOffset by mutableFloatStateOf(initialDragOffset) 104 105 private var _dragOffset by mutableFloatStateOf(initialDragOffset) 106 107 override var dragOffset: Float 108 get() = _dragOffset 109 /** 110 * Updates the [dragOffset]. 111 * 112 * This flips the [direction], if the [value] is further than [directionChangeSlop] away 113 * from the furthest recorded value regarding to the current [direction]. 114 */ 115 set(value) { 116 _dragOffset = value 117 this.direction = 118 when (direction) { 119 InputDirection.Max -> { 120 if (furthestDragOffset - value > directionChangeSlop) { 121 furthestDragOffset = value 122 InputDirection.Min 123 } else { 124 furthestDragOffset = max(value, furthestDragOffset) 125 InputDirection.Max 126 } 127 } 128 129 InputDirection.Min -> { 130 if (value - furthestDragOffset > directionChangeSlop) { 131 furthestDragOffset = value 132 InputDirection.Max 133 } else { 134 furthestDragOffset = min(value, furthestDragOffset) 135 InputDirection.Min 136 } 137 } 138 } 139 } 140 141 private var _directionChangeSlop by mutableFloatStateOf(directionChangeSlop) 142 143 var directionChangeSlop: Float 144 get() = _directionChangeSlop 145 146 /** 147 * This flips the [direction], if the current [direction] is further than the new 148 * directionChangeSlop [value] away from the furthest recorded value regarding to the 149 * current [direction]. 150 */ 151 set(value) { <lambda>null152 require(value > 0) { "directionChangeSlop must be greater than 0, was $value" } 153 154 _directionChangeSlop = value 155 156 when (direction) { 157 InputDirection.Max -> { 158 if (furthestDragOffset - dragOffset > directionChangeSlop) { 159 furthestDragOffset = dragOffset 160 direction = InputDirection.Min 161 } 162 } 163 InputDirection.Min -> { 164 if (dragOffset - furthestDragOffset > directionChangeSlop) { 165 furthestDragOffset = value 166 direction = InputDirection.Max 167 } 168 } 169 } 170 } 171 172 /** 173 * Sets [dragOffset] and [direction] to the specified values. 174 * 175 * This also resets memoized [furthestDragOffset], which is used to determine the direction 176 * change. 177 */ resetnull178 fun reset(dragOffset: Float, direction: InputDirection) { 179 this.dragOffset = dragOffset 180 this.direction = direction 181 this.furthestDragOffset = dragOffset 182 } 183 } 184