• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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