1 /*
<lambda>null2  * Copyright 2021 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 androidx.compose.foundation.gestures
18 
19 import androidx.compose.animation.core.AnimationSpec
20 import androidx.compose.animation.core.AnimationState
21 import androidx.compose.animation.core.AnimationVector
22 import androidx.compose.animation.core.AnimationVector1D
23 import androidx.compose.animation.core.AnimationVector2D
24 import androidx.compose.animation.core.AnimationVector4D
25 import androidx.compose.animation.core.Spring
26 import androidx.compose.animation.core.SpringSpec
27 import androidx.compose.animation.core.TwoWayConverter
28 import androidx.compose.animation.core.VectorConverter
29 import androidx.compose.animation.core.VectorizedAnimationSpec
30 import androidx.compose.animation.core.VectorizedFiniteAnimationSpec
31 import androidx.compose.animation.core.animateTo
32 import androidx.compose.foundation.MutatePriority
33 import androidx.compose.foundation.MutatorMutex
34 import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
35 import androidx.compose.foundation.internal.requirePrecondition
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.mutableStateOf
38 import androidx.compose.runtime.remember
39 import androidx.compose.runtime.rememberUpdatedState
40 import androidx.compose.ui.geometry.Offset
41 import kotlinx.coroutines.coroutineScope
42 
43 /**
44  * State of [transformable]. Allows for a granular control of how different gesture transformations
45  * are consumed by the user as well as to write custom transformation methods using [transform]
46  * suspend function.
47  */
48 @JvmDefaultWithCompatibility
49 interface TransformableState {
50     /**
51      * Call this function to take control of transformations and gain the ability to send transform
52      * events via [TransformScope.transformBy]. All actions that change zoom, pan or rotation values
53      * must be performed within a [transform] block (even if they don't call any other methods on
54      * this object) in order to guarantee that mutual exclusion is enforced.
55      *
56      * If [transform] is called from elsewhere with the [transformPriority] higher or equal to
57      * ongoing transform, ongoing transform will be canceled.
58      */
59     suspend fun transform(
60         transformPriority: MutatePriority = MutatePriority.Default,
61         block: suspend TransformScope.() -> Unit
62     )
63 
64     /**
65      * Whether this [TransformableState] is currently transforming by gesture or programmatically or
66      * not.
67      */
68     val isTransformInProgress: Boolean
69 }
70 
71 /** Scope used for suspending transformation operations */
72 @JvmDefaultWithCompatibility
73 interface TransformScope {
74     /**
75      * Attempts to transform by [zoomChange] in relative multiplied value, by [panChange] in pixels
76      * and by [rotationChange] in degrees.
77      *
78      * @param zoomChange scale factor multiplier change for zoom
79      * @param panChange panning offset change, in [Offset] pixels
80      * @param rotationChange change of the rotation in degrees
81      */
transformBynull82     fun transformBy(
83         zoomChange: Float = 1f,
84         panChange: Offset = Offset.Zero,
85         rotationChange: Float = 0f
86     )
87 }
88 
89 /**
90  * Default implementation of [TransformableState] interface that contains necessary information
91  * about the ongoing transformations and provides smooth transformation capabilities.
92  *
93  * This is the simplest way to set up a [transformable] modifier. When constructing this
94  * [TransformableState], you must provide a [onTransformation] lambda, which will be invoked
95  * whenever pan, zoom or rotation happens (by gesture input or any [TransformableState.transform]
96  * call) with the deltas from the previous event.
97  *
98  * @param onTransformation callback invoked when transformation occurs. The callback receives the
99  *   change from the previous event. It's relative scale multiplier for zoom, [Offset] in pixels for
100  *   pan and degrees for rotation. Callers should update their state in this lambda.
101  */
102 fun TransformableState(
103     onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
104 ): TransformableState = DefaultTransformableState(onTransformation)
105 
106 /**
107  * Create and remember default implementation of [TransformableState] interface that contains
108  * necessary information about the ongoing transformations and provides smooth transformation
109  * capabilities.
110  *
111  * This is the simplest way to set up a [transformable] modifier. When constructing this
112  * [TransformableState], you must provide a [onTransformation] lambda, which will be invoked
113  * whenever pan, zoom or rotation happens (by gesture input or any [TransformableState.transform]
114  * call) with the deltas from the previous event.
115  *
116  * @param onTransformation callback invoked when transformation occurs. The callback receives the
117  *   change from the previous event. It's relative scale multiplier for zoom, [Offset] in pixels for
118  *   pan and degrees for rotation. Callers should update their state in this lambda.
119  */
120 @Composable
121 fun rememberTransformableState(
122     onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
123 ): TransformableState {
124     val lambdaState = rememberUpdatedState(onTransformation)
125     return remember { TransformableState { z, p, r -> lambdaState.value.invoke(z, p, r) } }
126 }
127 
128 /**
129  * Animate zoom by a ratio of [zoomFactor] over the current size and suspend until its finished.
130  *
131  * @param zoomFactor ratio over the current size by which to zoom. For example, if [zoomFactor] is
132  *   `3f`, zoom will be increased 3 fold from the current value.
133  * @param animationSpec [AnimationSpec] to be used for animation
134  */
animateZoomBynull135 suspend fun TransformableState.animateZoomBy(
136     zoomFactor: Float,
137     animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)
138 ) {
139     requirePrecondition(zoomFactor > 0) { "zoom value should be greater than 0" }
140     var previous = 1f
141     transform {
142         AnimationState(initialValue = previous).animateTo(zoomFactor, animationSpec) {
143             val scaleFactor = if (previous == 0f) 1f else this.value / previous
144             transformBy(zoomChange = scaleFactor)
145             previous = this.value
146         }
147     }
148 }
149 
150 /**
151  * Animate rotate by a ratio of [degrees] clockwise and suspend until its finished.
152  *
153  * @param degrees the degrees by which to rotate clockwise
154  * @param animationSpec [AnimationSpec] to be used for animation
155  */
animateRotateBynull156 suspend fun TransformableState.animateRotateBy(
157     degrees: Float,
158     animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)
159 ) {
160     var previous = 0f
161     transform {
162         AnimationState(initialValue = previous).animateTo(degrees, animationSpec) {
163             val delta = this.value - previous
164             transformBy(rotationChange = delta)
165             previous = this.value
166         }
167     }
168 }
169 
170 /**
171  * Animate pan by [offset] Offset in pixels and suspend until its finished
172  *
173  * @param offset offset to pan, in pixels
174  * @param animationSpec [AnimationSpec] to be used for pan animation
175  */
animatePanBynull176 suspend fun TransformableState.animatePanBy(
177     offset: Offset,
178     animationSpec: AnimationSpec<Offset> = SpringSpec(stiffness = Spring.StiffnessLow)
179 ) {
180     var previous = Offset.Zero
181     transform {
182         AnimationState(typeConverter = Offset.VectorConverter, initialValue = previous).animateTo(
183             offset,
184             animationSpec
185         ) {
186             val delta = this.value - previous
187             transformBy(panChange = delta)
188             previous = this.value
189         }
190     }
191 }
192 
193 /**
194  * Animate zoom, pan, and rotation simultaneously and suspend until the animation is finished.
195  *
196  * Zoom is animated by a ratio of [zoomFactor] over the current size. Pan is animated by [panOffset]
197  * in pixels. Rotation is animated by the value of [rotationDegrees] clockwise. Any of these
198  * parameters can be set to a no-op value that will result in no animation of that parameter. The
199  * no-op values are the following: `1f` for [zoomFactor], `Offset.Zero` for [panOffset], and `0f`
200  * for [rotationDegrees].
201  *
202  * @sample androidx.compose.foundation.samples.TransformableAnimateBySample
203  * @param zoomFactor ratio over the current size by which to zoom. For example, if [zoomFactor] is
204  *   `3f`, zoom will be increased 3 fold from the current value.
205  * @param panOffset offset to pan, in pixels
206  * @param rotationDegrees the degrees by which to rotate clockwise
207  * @param zoomAnimationSpec [AnimationSpec] to be used for animating zoom
208  * @param panAnimationSpec [AnimationSpec] to be used for animating offset
209  * @param rotationAnimationSpec [AnimationSpec] to be used for animating rotation
210  */
animateBynull211 suspend fun TransformableState.animateBy(
212     zoomFactor: Float,
213     panOffset: Offset,
214     rotationDegrees: Float,
215     zoomAnimationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow),
216     panAnimationSpec: AnimationSpec<Offset> = SpringSpec(stiffness = Spring.StiffnessLow),
217     rotationAnimationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)
218 ) {
219     requirePrecondition(zoomFactor > 0) { "zoom value should be greater than 0" }
220     var previousState = AnimationData(zoom = 1f, offset = Offset.Zero, degrees = 0f)
221     val targetState = AnimationData(zoomFactor, panOffset, rotationDegrees)
222     val animationSpec =
223         DelegatingAnimationSpec(zoomAnimationSpec, panAnimationSpec, rotationAnimationSpec)
224     transform {
225         AnimationState(
226                 typeConverter = AnimationDataConverter,
227                 initialValue = previousState,
228                 initialVelocity = ZeroAnimationVelocity
229             )
230             .animateTo(targetState, animationSpec) {
231                 transformBy(
232                     zoomChange =
233                         if (previousState.zoom == 0f) 1f else value.zoom / previousState.zoom,
234                     rotationChange = value.degrees - previousState.degrees,
235                     panChange = value.offset - previousState.offset
236                 )
237                 previousState = value
238             }
239     }
240 }
241 
242 private val ZeroAnimationVelocity = AnimationData(zoom = 0f, offset = Offset.Zero, degrees = 0f)
243 
244 private class DelegatingAnimationSpec(
245     private val zoomAnimationSpec: AnimationSpec<Float>,
246     private val offsetAnimationSpec: AnimationSpec<Offset>,
247     private val rotationAnimationSpec: AnimationSpec<Float>
248 ) : AnimationSpec<AnimationData> {
vectorizenull249     override fun <V : AnimationVector> vectorize(
250         converter: TwoWayConverter<AnimationData, V>
251     ): VectorizedAnimationSpec<V> {
252         val vectorizedZoomAnimationSpec = zoomAnimationSpec.vectorize(Float.VectorConverter)
253         val vectorizedOffsetAnimationSpec = offsetAnimationSpec.vectorize(Offset.VectorConverter)
254         val vectorizedRotationAnimationSpec = rotationAnimationSpec.vectorize(Float.VectorConverter)
255         return object : VectorizedFiniteAnimationSpec<V> {
256             override fun getDurationNanos(
257                 initialValue: V,
258                 targetValue: V,
259                 initialVelocity: V
260             ): Long {
261                 val initialAnimationData = converter.convertFromVector(initialValue)
262                 val targetAnimationData = converter.convertFromVector(targetValue)
263                 val initialVelocityAnimationData = converter.convertFromVector(initialVelocity)
264 
265                 return maxOf(
266                     vectorizedZoomAnimationSpec.getDurationNanos(
267                         initialAnimationData.zoomVector(),
268                         targetAnimationData.zoomVector(),
269                         initialVelocityAnimationData.zoomVector()
270                     ),
271                     vectorizedOffsetAnimationSpec.getDurationNanos(
272                         initialAnimationData.offsetVector(),
273                         targetAnimationData.offsetVector(),
274                         initialVelocityAnimationData.offsetVector()
275                     ),
276                     vectorizedRotationAnimationSpec.getDurationNanos(
277                         initialAnimationData.degreesVector(),
278                         targetAnimationData.degreesVector(),
279                         initialVelocityAnimationData.degreesVector()
280                     )
281                 )
282             }
283 
284             override fun getVelocityFromNanos(
285                 playTimeNanos: Long,
286                 initialValue: V,
287                 targetValue: V,
288                 initialVelocity: V
289             ): V {
290                 val initialAnimationData = converter.convertFromVector(initialValue)
291                 val targetAnimationData = converter.convertFromVector(targetValue)
292                 val initialVelocityAnimationData = converter.convertFromVector(initialVelocity)
293 
294                 val zoomVelocity =
295                     vectorizedZoomAnimationSpec.getVelocityFromNanos(
296                         playTimeNanos,
297                         initialAnimationData.zoomVector(),
298                         targetAnimationData.zoomVector(),
299                         initialVelocityAnimationData.zoomVector()
300                     )
301                 val offsetVelocity =
302                     vectorizedOffsetAnimationSpec.getVelocityFromNanos(
303                         playTimeNanos,
304                         initialAnimationData.offsetVector(),
305                         targetAnimationData.offsetVector(),
306                         initialVelocityAnimationData.offsetVector()
307                     )
308                 val rotationVelocity =
309                     vectorizedRotationAnimationSpec.getVelocityFromNanos(
310                         playTimeNanos,
311                         initialAnimationData.degreesVector(),
312                         targetAnimationData.degreesVector(),
313                         initialVelocityAnimationData.degreesVector()
314                     )
315 
316                 return packToAnimationVector(zoomVelocity, offsetVelocity, rotationVelocity)
317             }
318 
319             override fun getValueFromNanos(
320                 playTimeNanos: Long,
321                 initialValue: V,
322                 targetValue: V,
323                 initialVelocity: V
324             ): V {
325                 val initialAnimationData = converter.convertFromVector(initialValue)
326                 val targetAnimationData = converter.convertFromVector(targetValue)
327                 val initialVelocityAnimationData = converter.convertFromVector(initialVelocity)
328 
329                 val zoomValue =
330                     vectorizedZoomAnimationSpec.getValueFromNanos(
331                         playTimeNanos,
332                         initialAnimationData.zoomVector(),
333                         targetAnimationData.zoomVector(),
334                         initialVelocityAnimationData.zoomVector()
335                     )
336                 val offsetValue =
337                     vectorizedOffsetAnimationSpec.getValueFromNanos(
338                         playTimeNanos,
339                         initialAnimationData.offsetVector(),
340                         targetAnimationData.offsetVector(),
341                         initialVelocityAnimationData.offsetVector()
342                     )
343                 val rotationValue =
344                     vectorizedRotationAnimationSpec.getValueFromNanos(
345                         playTimeNanos,
346                         initialAnimationData.degreesVector(),
347                         targetAnimationData.degreesVector(),
348                         initialVelocityAnimationData.degreesVector()
349                     )
350 
351                 return packToAnimationVector(zoomValue, offsetValue, rotationValue)
352             }
353 
354             private fun AnimationData.zoomVector() =
355                 Float.VectorConverter.convertToVector(this.zoom)
356 
357             private fun AnimationData.offsetVector() =
358                 Offset.VectorConverter.convertToVector(Offset(this.offset.x, this.offset.y))
359 
360             private fun AnimationData.degreesVector() =
361                 Float.VectorConverter.convertToVector(this.degrees)
362 
363             private fun packToAnimationVector(
364                 zoom: AnimationVector1D,
365                 offset: AnimationVector2D,
366                 rotation: AnimationVector1D
367             ): V =
368                 converter.convertToVector(
369                     AnimationData(zoom.value, Offset(offset.v1, offset.v2), rotation.value)
370                 )
371         }
372     }
373 }
374 
375 private object AnimationDataConverter : TwoWayConverter<AnimationData, AnimationVector4D> {
376     override val convertToVector: (AnimationData) -> AnimationVector4D
<lambda>null377         get() = { AnimationVector4D(it.zoom, it.offset.x, it.offset.y, it.degrees) }
378 
379     override val convertFromVector: (AnimationVector4D) -> AnimationData
<lambda>null380         get() = { AnimationData(zoom = it.v1, offset = Offset(it.v2, it.v3), degrees = it.v4) }
381 }
382 
383 private data class AnimationData(val zoom: Float, val offset: Offset, val degrees: Float)
384 
385 /**
386  * Zoom without animation by a ratio of [zoomFactor] over the current size and suspend until it's
387  * set.
388  *
389  * @param zoomFactor ratio over the current size by which to zoom
390  */
<lambda>null391 suspend fun TransformableState.zoomBy(zoomFactor: Float) = transform {
392     transformBy(zoomFactor, Offset.Zero, 0f)
393 }
394 
395 /**
396  * Rotate without animation by a [degrees] degrees and suspend until it's set.
397  *
398  * @param degrees degrees by which to rotate
399  */
<lambda>null400 suspend fun TransformableState.rotateBy(degrees: Float) = transform {
401     transformBy(1f, Offset.Zero, degrees)
402 }
403 
404 /**
405  * Pan without animation by a [offset] Offset in pixels and suspend until it's set.
406  *
407  * @param offset offset in pixels by which to pan
408  */
<lambda>null409 suspend fun TransformableState.panBy(offset: Offset) = transform { transformBy(1f, offset, 0f) }
410 
411 /**
412  * Stop and suspend until any ongoing [TransformableState.transform] with priority
413  * [terminationPriority] or lower is terminated.
414  *
415  * @param terminationPriority transformation that runs with this priority or lower will be stopped
416  */
stopTransformationnull417 suspend fun TransformableState.stopTransformation(
418     terminationPriority: MutatePriority = MutatePriority.Default
419 ) {
420     this.transform(terminationPriority) {
421         // do nothing, just lock the mutex so other scroll actors are cancelled
422     }
423 }
424 
425 private class DefaultTransformableState(
426     val onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
427 ) : TransformableState {
428 
429     private val transformScope: TransformScope =
430         object : TransformScope {
transformBynull431             override fun transformBy(zoomChange: Float, panChange: Offset, rotationChange: Float) =
432                 onTransformation(zoomChange, panChange, rotationChange)
433         }
434 
435     private val transformMutex = MutatorMutex()
436 
437     private val isTransformingState = mutableStateOf(false)
438 
439     override suspend fun transform(
440         transformPriority: MutatePriority,
441         block: suspend TransformScope.() -> Unit
442     ): Unit = coroutineScope {
443         transformMutex.mutateWith(transformScope, transformPriority) {
444             isTransformingState.value = true
445             try {
446                 block()
447             } finally {
448                 isTransformingState.value = false
449             }
450         }
451     }
452 
453     override val isTransformInProgress: Boolean
454         get() = isTransformingState.value
455 }
456