1 /*
<lambda>null2  * Copyright (C) 2022 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.constraintlayout.compose
18 
19 import androidx.annotation.FloatRange
20 import androidx.annotation.IntRange
21 import androidx.compose.foundation.layout.LayoutScopeMarker
22 import androidx.compose.ui.unit.Dp
23 import androidx.compose.ui.unit.dp
24 import androidx.constraintlayout.core.parser.CLArray
25 import androidx.constraintlayout.core.parser.CLContainer
26 import androidx.constraintlayout.core.parser.CLNumber
27 import androidx.constraintlayout.core.parser.CLObject
28 import androidx.constraintlayout.core.parser.CLString
29 import kotlin.properties.ObservableProperty
30 import kotlin.reflect.KProperty
31 
32 /**
33  * Defines the interpolation parameters between the [ConstraintSet]s to achieve fine-tuned
34  * animations.
35  *
36  * @param from The name of the initial [ConstraintSet]. Should correspond to a named [ConstraintSet]
37  *   when added as part of a [MotionScene] with [MotionSceneScope.addTransition].
38  * @param to The name of the target [ConstraintSet]. Should correspond to a named [ConstraintSet]
39  *   when added as part of a [MotionScene] with [MotionSceneScope.addTransition].
40  * @param content Lambda to define the Transition parameters on the given [TransitionScope].
41  */
42 @ExperimentalMotionApi
43 fun Transition(
44     from: String = "start",
45     to: String = "end",
46     content: TransitionScope.() -> Unit
47 ): Transition {
48     val transitionScope = TransitionScope(from, to)
49     transitionScope.content()
50     return TransitionImpl(transitionScope.getObject())
51 }
52 
53 /**
54  * Scope where [Transition] parameters are defined.
55  *
56  * Here, you may define multiple KeyFrames for specific [ConstrainedLayoutReference]s, as well was
57  * enabling [OnSwipe] handling.
58  *
59  * @see keyAttributes
60  * @see keyPositions
61  * @see keyCycles
62  */
63 @ExperimentalMotionApi
64 @LayoutScopeMarker
65 class TransitionScope internal constructor(private val from: String, private val to: String) {
66     private val containerObject = CLObject(charArrayOf())
67 
68     private val keyFramesObject = CLObject(charArrayOf())
69     private val keyAttributesArray = CLArray(charArrayOf())
70     private val keyPositionsArray = CLArray(charArrayOf())
71     private val keyCyclesArray = CLArray(charArrayOf())
72 
73     private val onSwipeObject = CLObject(charArrayOf())
74 
resetnull75     internal fun reset() {
76         containerObject.clear()
77         keyFramesObject.clear()
78         keyAttributesArray.clear()
79         onSwipeObject.clear()
80     }
81 
addKeyAttributesIfMissingnull82     private fun addKeyAttributesIfMissing() {
83         containerObject.put("KeyFrames", keyFramesObject)
84         keyFramesObject.put("KeyAttributes", keyAttributesArray)
85     }
86 
addKeyPositionsIfMissingnull87     private fun addKeyPositionsIfMissing() {
88         containerObject.put("KeyFrames", keyFramesObject)
89         keyFramesObject.put("KeyPositions", keyPositionsArray)
90     }
91 
addKeyCyclesIfMissingnull92     private fun addKeyCyclesIfMissing() {
93         containerObject.put("KeyFrames", keyFramesObject)
94         keyFramesObject.put("KeyCycles", keyCyclesArray)
95     }
96 
97     /**
98      * The default [Arc] shape for animated layout movement.
99      *
100      * [Arc.None] by default.
101      */
102     var motionArc: Arc = Arc.None
103 
104     /**
105      * When not null, enables animating through the transition with touch input.
106      *
107      * Example:
108      * ```
109      *  MotionLayout(
110      *      motionScene = MotionScene {
111      *          val textRef = createRefFor("text")
112      *          defaultTransition(
113      *              from = constraintSet {
114      *                  constrain(textRef) {
115      *                      top.linkTo(parent.top)
116      *                  }
117      *              },
118      *              to = constraintSet {
119      *                  constrain(textRef) {
120      *                      bottom.linkTo(parent.bottom)
121      *                  }
122      *              }
123      *          ) {
124      *              onSwipe = OnSwipe(
125      *                  anchor = textRef,
126      *                  side = SwipeSide.Middle,
127      *                  direction = SwipeDirection.Down
128      *              )
129      *          }
130      *      },
131      *      progress = 0f, // OnSwipe handles the progress, so this should be constant to avoid conflict
132      *      modifier = Modifier.fillMaxSize()
133      *  ) {
134      *      Text("Hello, World!", Modifier.layoutId("text"))
135      *  }
136      * ```
137      *
138      * @see OnSwipe
139      */
140     var onSwipe: OnSwipe? = null
141 
142     /**
143      * Defines the maximum delay (in progress value) between a group of staggered widgets.
144      *
145      * The amount of delay for each widget is decided based on its weight. Where the widget with the
146      * lowest weight will receive the full delay. A negative [maxStaggerDelay] value inverts this
147      * logic, so that the widget with the highest weight will receive the full delay.
148      *
149      * By default, the weight of each widget is calculated as the Manhattan Distance from the
150      * top-left corner of the layout. You may set custom weights using
151      * [MotionSceneScope.staggeredWeight] on a per-widget basis, this essentially allows you to set
152      * a custom staggering order. Note that when you set custom weights, widgets without a custom
153      * weight will be ignored for this calculation and will animate without delay.
154      *
155      * The remaining widgets will receive a portion of this delay, based on their weight calculated
156      * against each other.
157      *
158      * This is the formula to calculate the progress delay for a widget **i**, where
159      * **Max/MinWeight** is defined by the maximum and minimum calculated (or custom) weight:
160      * ```
161      * progressDelay[i] = maxStaggerDelay * (1 - ((weight[i] - MinWeight) / (MaxWeight - MinWeight)))
162      * ```
163      *
164      * To simplify, this is the formula normalized against **MinWeight**:
165      * ```
166      * progressDelay[i] = maxStaggerDelay * (1 - weight[i] / MaxWeight)
167      * ```
168      *
169      * Example:
170      *
171      * Given three widgets with custom weights `[1, 2, 3]` and [maxStaggerDelay] = 0.7f.
172      * - Widget0 will start animating at `progress == 0.7f` for having the lowest weight.
173      * - Widget1 will start animating at `progress == 0.35f`
174      * - Widget2 will start animating at `progress == 0.0f`
175      *
176      * This is because the weights are distributed linearly among the widgets.
177      */
178     @FloatRange(-1.0, 1.0, fromInclusive = false, toInclusive = false)
179     var maxStaggerDelay: Float = 0.0f
180 
181     /**
182      * Define KeyAttribute KeyFrames for the given [targets].
183      *
184      * Set multiple KeyFrames with [KeyAttributesScope.frame].
185      */
keyAttributesnull186     fun keyAttributes(
187         vararg targets: ConstrainedLayoutReference,
188         keyAttributesContent: KeyAttributesScope.() -> Unit
189     ) {
190         val scope = KeyAttributesScope(*targets)
191         keyAttributesContent(scope)
192         addKeyAttributesIfMissing()
193         keyAttributesArray.add(scope.keyFramePropsObject)
194     }
195 
196     /**
197      * Define KeyPosition KeyFrames for the given [targets].
198      *
199      * Set multiple KeyFrames with [KeyPositionsScope.frame].
200      */
keyPositionsnull201     fun keyPositions(
202         vararg targets: ConstrainedLayoutReference,
203         keyPositionsContent: KeyPositionsScope.() -> Unit
204     ) {
205         val scope = KeyPositionsScope(*targets)
206         keyPositionsContent(scope)
207         addKeyPositionsIfMissing()
208         keyPositionsArray.add(scope.keyFramePropsObject)
209     }
210 
211     /**
212      * Define KeyCycle KeyFrames for the given [targets].
213      *
214      * Set multiple KeyFrames with [KeyCyclesScope.frame].
215      */
keyCyclesnull216     fun keyCycles(
217         vararg targets: ConstrainedLayoutReference,
218         keyCyclesContent: KeyCyclesScope.() -> Unit
219     ) {
220         val scope = KeyCyclesScope(*targets)
221         keyCyclesContent(scope)
222         addKeyCyclesIfMissing()
223         keyCyclesArray.add(scope.keyFramePropsObject)
224     }
225 
226     /**
227      * Creates one [ConstrainedLayoutReference] corresponding to the [ConstraintLayout] element with
228      * [id].
229      */
createRefFornull230     fun createRefFor(id: Any): ConstrainedLayoutReference = ConstrainedLayoutReference(id)
231 
232     internal fun getObject(): CLObject {
233         containerObject.putString("pathMotionArc", motionArc.name)
234         containerObject.putString("from", from)
235         containerObject.putString("to", to)
236         // TODO: Uncomment once we decide how to deal with Easing discrepancy from user driven
237         //  `progress` value. Eg: `animateFloat(tween(duration, LinearEasing))`
238         //        containerObject.putString("interpolator", easing.name)
239         //        containerObject.putNumber("duration", durationMs.toFloat())
240         containerObject.putNumber("staggered", maxStaggerDelay)
241         onSwipe?.let {
242             containerObject.put("onSwipe", onSwipeObject)
243             onSwipeObject.putString("direction", it.direction.name)
244             onSwipeObject.putNumber("scale", it.dragScale)
245             it.dragAround?.id?.let { id -> onSwipeObject.putString("around", id.toString()) }
246             it.limitBoundsTo?.id?.let { id ->
247                 onSwipeObject.putString("limitBounds", id.toString())
248             }
249             onSwipeObject.putNumber("threshold", it.dragThreshold)
250             onSwipeObject.putString("anchor", it.anchor.id.toString())
251             onSwipeObject.putString("side", it.side.name)
252             onSwipeObject.putString("touchUp", it.onTouchUp.name)
253             onSwipeObject.putString("mode", it.mode.name)
254             onSwipeObject.putNumber("maxVelocity", it.mode.maxVelocity)
255             onSwipeObject.putNumber("maxAccel", it.mode.maxAcceleration)
256             onSwipeObject.putNumber("springMass", it.mode.springMass)
257             onSwipeObject.putNumber("springStiffness", it.mode.springStiffness)
258             onSwipeObject.putNumber("springDamping", it.mode.springDamping)
259             onSwipeObject.putNumber("stopThreshold", it.mode.springThreshold)
260             onSwipeObject.putString("springBoundary", it.mode.springBoundary.name)
261         }
262         return containerObject
263     }
264 }
265 
266 /**
267  * The base/common scope for KeyFrames.
268  *
269  * Each KeyFrame may have multiple frames and multiple properties for each frame. The frame values
270  * should be registered on [framesContainer] and the corresponding properties changes on
271  * [keyFramePropsObject].
272  */
273 @ExperimentalMotionApi
274 sealed class BaseKeyFramesScope(vararg targets: ConstrainedLayoutReference) {
<lambda>null275     internal val keyFramePropsObject = CLObject(charArrayOf()).apply { clear() }
276 
277     private val targetsContainer = CLArray(charArrayOf())
278     internal val framesContainer = CLArray(charArrayOf())
279 
280     /** The [Easing] curve to apply for the KeyFrames defined in this scope. */
281     var easing: Easing by addNameOnPropertyChange(Easing.Standard, "transitionEasing")
282 
283     init {
284         keyFramePropsObject.put("target", targetsContainer)
285         keyFramePropsObject.put("frames", framesContainer)
<lambda>null286         targets.forEach {
287             val targetChars = it.id.toString().toCharArray()
288             targetsContainer.add(
289                 CLString(targetChars).apply {
290                     start = 0
291                     end = targetChars.size.toLong() - 1
292                 }
293             )
294         }
295     }
296 
297     /**
298      * Registers changes of this property to [keyFramePropsObject]. Where the key is the name of the
299      * property. Use [nameOverride] to apply a different key.
300      */
addNameOnPropertyChangenull301     internal fun <E : NamedPropertyOrValue?> addNameOnPropertyChange(
302         initialValue: E,
303         nameOverride: String? = null
304     ) =
305         object : ObservableProperty<E>(initialValue) {
306             override fun afterChange(property: KProperty<*>, oldValue: E, newValue: E) {
307                 val name = nameOverride ?: property.name
308                 if (newValue != null) {
309                     keyFramePropsObject.putString(name, newValue.name)
310                 }
311             }
312         }
313 }
314 
315 /**
316  * Fake private implementation of [BaseKeyFramesScope] to prevent exhaustive `when` usages of
317  * [BaseKeyFramesScope], while `sealed` prevents undesired inheritance of [BaseKeyFramesScope].
318  */
319 @OptIn(ExperimentalMotionApi::class) private class FakeKeyFramesScope : BaseKeyFramesScope()
320 
321 /**
322  * Scope where multiple attribute KeyFrames may be defined.
323  *
324  * @see frame
325  */
326 @ExperimentalMotionApi
327 @LayoutScopeMarker
328 class KeyAttributesScope internal constructor(vararg targets: ConstrainedLayoutReference) :
329     BaseKeyFramesScope(*targets) {
330 
331     /**
332      * Define KeyAttribute values at a given KeyFrame, where the [frame] is a specific progress
333      * value from 0 to 100.
334      *
335      * All properties set on [KeyAttributeScope] for this [frame] should also be set on other
336      * [frame] declarations made within this scope.
337      */
framenull338     fun frame(@IntRange(0, 100) frame: Int, keyFrameContent: KeyAttributeScope.() -> Unit) {
339         val scope = KeyAttributeScope()
340         keyFrameContent(scope)
341         framesContainer.add(CLNumber(frame.toFloat()))
342         scope.addToContainer(keyFramePropsObject)
343     }
344 }
345 
346 /**
347  * Scope where multiple position KeyFrames may be defined.
348  *
349  * @see frame
350  */
351 @ExperimentalMotionApi
352 @LayoutScopeMarker
353 class KeyPositionsScope internal constructor(vararg targets: ConstrainedLayoutReference) :
354     BaseKeyFramesScope(*targets) {
355     /**
356      * Sets the coordinate space in which KeyPositions are defined.
357      *
358      * [RelativePosition.Delta] by default.
359      */
360     var type by addNameOnPropertyChange(RelativePosition.Delta)
361 
362     /**
363      * Define KeyPosition values at a given KeyFrame, where the [frame] is a specific progress value
364      * from 0 to 100.
365      *
366      * All properties set on [KeyPositionScope] for this [frame] should also be set on other [frame]
367      * declarations made within this scope.
368      */
framenull369     fun frame(@IntRange(0, 100) frame: Int, keyFrameContent: KeyPositionScope.() -> Unit) {
370         val scope = KeyPositionScope()
371         keyFrameContent(scope)
372         framesContainer.add(CLNumber(frame.toFloat()))
373         scope.addToContainer(keyFramePropsObject)
374     }
375 }
376 
377 /**
378  * Scope where multiple cycling attribute KeyFrames may be defined.
379  *
380  * @see frame
381  */
382 @ExperimentalMotionApi
383 @LayoutScopeMarker
384 class KeyCyclesScope internal constructor(vararg targets: ConstrainedLayoutReference) :
385     BaseKeyFramesScope(*targets) {
386 
387     /**
388      * Define KeyCycle values at a given KeyFrame, where the [frame] is a specific progress value
389      * from 0 to 100.
390      *
391      * All properties set on [KeyCycleScope] for this [frame] should also be set on other [frame]
392      * declarations made within this scope.
393      */
framenull394     fun frame(@IntRange(0, 100) frame: Int, keyFrameContent: KeyCycleScope.() -> Unit) {
395         val scope = KeyCycleScope()
396         keyFrameContent(scope)
397         framesContainer.add(CLNumber(frame.toFloat()))
398         scope.addToContainer(keyFramePropsObject)
399     }
400 }
401 
402 /**
403  * The base/common scope for individual KeyFrame declarations.
404  *
405  * Properties should be registered on [keyFramePropertiesValue], however, custom properties must use
406  * [customPropertiesValue].
407  */
408 @ExperimentalMotionApi
409 sealed class BaseKeyFrameScope {
410     /**
411      * PropertyName-Value map for the properties of each type of key frame.
412      *
413      * The values are for a singular unspecified frame.
414      */
415     private val keyFramePropertiesValue = mutableMapOf<String, Any>()
416 
417     /**
418      * PropertyName-Value map for user-defined values.
419      *
420      * Typically used on KeyAttributes only.
421      */
422     internal val customPropertiesValue = mutableMapOf<String, Any>()
423 
424     /**
425      * When changed, updates the value of type [T] on the [keyFramePropertiesValue] map.
426      *
427      * Where the Key is the property's name unless [nameOverride] is not null.
428      */
addOnPropertyChangenull429     protected fun <T> addOnPropertyChange(initialValue: T, nameOverride: String? = null) =
430         object : ObservableProperty<T>(initialValue) {
431             override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
432                 if (newValue != null) {
433                     keyFramePropertiesValue[nameOverride ?: property.name] = newValue
434                 } else {
435                     keyFramePropertiesValue.remove(nameOverride ?: property.name)
436                 }
437             }
438         }
439 
440     /**
441      * Property delegate that updates the [keyFramePropertiesValue] map on value changes.
442      *
443      * Where the Key is the property's name unless [nameOverride] is not null.
444      *
445      * The value is the String given by [NamedPropertyOrValue.name].
446      *
447      * Use when declaring properties that have a named value.
448      *
449      * E.g.: `var curveFit: CurveFit? by addNameOnPropertyChange(null)`
450      */
451     @Suppress("EXPOSED_TYPE_PARAMETER_BOUND_DEPRECATION_WARNING")
addNameOnPropertyChangenull452     protected fun <E : NamedPropertyOrValue?> addNameOnPropertyChange(
453         initialValue: E,
454         nameOverride: String? = null
455     ) =
456         object : ObservableProperty<E>(initialValue) {
457             override fun afterChange(property: KProperty<*>, oldValue: E, newValue: E) {
458                 val name = nameOverride ?: property.name
459                 if (newValue != null) {
460                     keyFramePropertiesValue[name] = newValue.name
461                 }
462             }
463         }
464 
465     /**
466      * Adds the property maps to the given container.
467      *
468      * Where every value is treated as part of array.
469      */
addToContainernull470     internal fun addToContainer(container: CLContainer) {
471         container.putValuesAsArrayElements(keyFramePropertiesValue)
472         val customPropsObject =
473             container.getObjectOrNull("custom")
474                 ?: run {
475                     val custom = CLObject(charArrayOf())
476                     container.put("custom", custom)
477                     custom
478                 }
479         customPropsObject.putValuesAsArrayElements(customPropertiesValue)
480     }
481 
482     /**
483      * Adds the values from [propertiesSource] to the [CLContainer].
484      *
485      * Each value will be added as a new element of their corresponding array (given by the Key,
486      * which is the name of the affected property).
487      */
CLContainernull488     private fun CLContainer.putValuesAsArrayElements(propertiesSource: Map<String, Any>) {
489         propertiesSource.forEach { (name, value) ->
490             val array = this.getArrayOrCreate(name)
491             when (value) {
492                 is String -> {
493                     val stringChars = value.toCharArray()
494                     array.add(
495                         CLString(stringChars).apply {
496                             start = 0
497                             end = stringChars.size.toLong() - 1
498                         }
499                     )
500                 }
501                 is Dp -> {
502                     array.add(CLNumber(value.value))
503                 }
504                 is Number -> {
505                     array.add(CLNumber(value.toFloat()))
506                 }
507             }
508         }
509     }
510 }
511 
512 /**
513  * Fake private implementation of [BaseKeyFrameScope] to prevent exhaustive `when` usages of
514  * [BaseKeyFrameScope], while `sealed` prevents undesired inheritance of [BaseKeyFrameScope].
515  */
516 @OptIn(ExperimentalMotionApi::class) private class FakeKeyFrameScope : BaseKeyFrameScope()
517 
518 /**
519  * Scope to define KeyFrame attributes.
520  *
521  * Supports transform parameters: alpha, scale, rotation and translation.
522  *
523  * You may also define custom properties when called within a [MotionSceneScope].
524  *
525  * @see [MotionSceneScope.customFloat]
526  */
527 @ExperimentalMotionApi
528 @LayoutScopeMarker
529 class KeyAttributeScope internal constructor() : BaseKeyFrameScope() {
530     var alpha by addOnPropertyChange(1f, "alpha")
531     var scaleX by addOnPropertyChange(1f, "scaleX")
532     var scaleY by addOnPropertyChange(1f, "scaleY")
533     var rotationX by addOnPropertyChange(0f, "rotationX")
534     var rotationY by addOnPropertyChange(0f, "rotationY")
535     var rotationZ by addOnPropertyChange(0f, "rotationZ")
536     var translationX: Dp by addOnPropertyChange(0.dp, "translationX")
537     var translationY: Dp by addOnPropertyChange(0.dp, "translationY")
538     var translationZ: Dp by addOnPropertyChange(0.dp, "translationZ")
539 }
540 
541 /**
542  * Scope to define KeyFrame positions.
543  *
544  * These are modifications on the widget's position and size relative to its final state on the
545  * current transition.
546  */
547 @ExperimentalMotionApi
548 @LayoutScopeMarker
549 class KeyPositionScope internal constructor() : BaseKeyFrameScope() {
550     /**
551      * The position as a percentage of the X axis of the current coordinate space.
552      *
553      * Where 0 is the position at the **start** [ConstraintSet] and 1 is at the **end**
554      * [ConstraintSet].
555      *
556      * The coordinate space is defined by [KeyPositionsScope.type].
557      */
558     var percentX by addOnPropertyChange(1f)
559 
560     /**
561      * The position as a percentage of the Y axis of the current coordinate space.
562      *
563      * Where 0 is the position at the **start** [ConstraintSet] and 1 is at the **end**
564      * [ConstraintSet].
565      *
566      * The coordinate space is defined by [KeyPositionsScope.type].
567      */
568     var percentY by addOnPropertyChange(1f)
569 
570     /** The width as a percentage of the width at the end [ConstraintSet]. */
571     var percentWidth by addOnPropertyChange(1f)
572 
573     /** The height as a percentage of the height at the end [ConstraintSet]. */
574     var percentHeight by addOnPropertyChange(0f)
575 
576     /** Type of fit applied to the curve. [CurveFit.Spline] by default. */
577     var curveFit: CurveFit? by addNameOnPropertyChange(null)
578 }
579 
580 /**
581  * Scope to define cycling KeyFrames.
582  *
583  * [KeyCycleScope] allows you to apply wave-based transforms, defined by [period], [offset] and
584  * [phase]. A sinusoidal wave is used by default.
585  */
586 @ExperimentalMotionApi
587 @LayoutScopeMarker
588 class KeyCycleScope internal constructor() : BaseKeyFrameScope() {
589     var alpha by addOnPropertyChange(1f)
590     var scaleX by addOnPropertyChange(1f)
591     var scaleY by addOnPropertyChange(1f)
592     var rotationX by addOnPropertyChange(0f)
593     var rotationY by addOnPropertyChange(0f)
594     var rotationZ by addOnPropertyChange(0f)
595     var translationX: Dp by addOnPropertyChange(0.dp)
596     var translationY: Dp by addOnPropertyChange(0.dp)
597     var translationZ: Dp by addOnPropertyChange(0.dp)
598     var period by addOnPropertyChange(0f)
599     var offset by addOnPropertyChange(0f)
600     var phase by addOnPropertyChange(0f)
601 
602     // TODO: Add Wave Shape & Custom Wave
603 }
604 
605 internal interface NamedPropertyOrValue {
606     val name: String
607 }
608 
609 /**
610  * Defines the OnSwipe behavior for a [Transition].
611  *
612  * When swiping, the [MotionLayout] is updated to a progress value so that the given
613  * [ConstrainedLayoutReference] is laid out in a position corresponding to the drag.
614  *
615  * In other words, [OnSwipe] allows you to drive [MotionLayout] by dragging a specific
616  * [ConstrainedLayoutReference].
617  *
618  * @param anchor The [ConstrainedLayoutReference] to track through touch input.
619  * @param side Side of the bounds to track, this is to account for when the tracked widget changes
620  *   size during the [Transition].
621  * @param direction Expected swipe direction to start the animation through touch handling.
622  *   Typically, this is the direction the widget takes to the end [ConstraintSet].
623  * @param dragScale Scaling factor applied on the dragged distance, meaning that the larger the
624  *   scaling value, the shorter distance is required to animate the entire Transition. 1f by
625  *   default.
626  * @param dragThreshold Distance in pixels required to consider the drag as initiated. 10 by
627  *   default.
628  * @param dragAround When not-null, causes the [anchor] to be dragged around the center of the given
629  *   [ConstrainedLayoutReference] in a circular motion.
630  * @param limitBoundsTo When not-null, the touch handling won't be initiated unless it's within the
631  *   bounds of the given [ConstrainedLayoutReference]. Useful to deal with touch handling conflicts.
632  * @param onTouchUp Defines what behavior MotionLayout should have when the drag event is
633  *   interrupted by TouchUp. [SwipeTouchUp.AutoComplete] by default.
634  * @param mode Describes how MotionLayout animates during [onTouchUp]. [SwipeMode.velocity] by
635  *   default.
636  */
637 @ExperimentalMotionApi
638 class OnSwipe(
639     val anchor: ConstrainedLayoutReference,
640     val side: SwipeSide,
641     val direction: SwipeDirection,
642     val dragScale: Float = 1f,
643     val dragThreshold: Float = 10f,
644     val dragAround: ConstrainedLayoutReference? = null,
645     val limitBoundsTo: ConstrainedLayoutReference? = null,
646     val onTouchUp: SwipeTouchUp = SwipeTouchUp.AutoComplete,
647     val mode: SwipeMode = SwipeMode.velocity(),
648 )
649 
650 /**
651  * Supported Easing curves.
652  *
653  * You may define your own Cubic-bezier easing curve with [cubic].
654  */
655 @ExperimentalMotionApi
656 class Easing internal constructor(override val name: String) : NamedPropertyOrValue {
657     companion object {
658         /**
659          * Standard [Easing] curve, also known as: Ease in, ease out.
660          *
661          * Defined as `cubic(0.4f, 0.0f, 0.2f, 1f)`.
662          */
663         val Standard = Easing("standard")
664 
665         /**
666          * Acceleration [Easing] curve, also known as: Ease in.
667          *
668          * Defined as `cubic(0.4f, 0.05f, 0.8f, 0.7f)`.
669          */
670         val Accelerate = Easing("accelerate")
671 
672         /**
673          * Deceleration [Easing] curve, also known as: Ease out.
674          *
675          * Defined as `cubic(0.0f, 0.0f, 0.2f, 0.95f)`.
676          */
677         val Decelerate = Easing("decelerate")
678 
679         /**
680          * Linear [Easing] curve.
681          *
682          * Defined as `cubic(1f, 1f, 0f, 0f)`.
683          */
684         val Linear = Easing("linear")
685 
686         /**
687          * Anticipate is an [Easing] curve with a small negative overshoot near the start of the
688          * motion.
689          *
690          * Defined as `cubic(0.36f, 0f, 0.66f, -0.56f)`.
691          */
692         val Anticipate = Easing("anticipate")
693 
694         /**
695          * Overshoot is an [Easing] curve with a small positive overshoot near the end of the
696          * motion.
697          *
698          * Defined as `cubic(0.34f, 1.56f, 0.64f, 1f)`.
699          */
700         val Overshoot = Easing("overshoot")
701 
702         /**
703          * Defines a Cubic-Bezier curve where the points P1 and P2 are at the given coordinate
704          * ratios.
705          *
706          * P1 and P2 are typically defined within (0f, 0f) and (1f, 1f), but may be assigned beyond
707          * these values for overshoot curves.
708          *
709          * @param x1 X-axis value for P1. Value is typically defined within 0f-1f.
710          * @param y1 Y-axis value for P1. Value is typically defined within 0f-1f.
711          * @param x2 X-axis value for P2. Value is typically defined within 0f-1f.
712          * @param y2 Y-axis value for P2. Value is typically defined within 0f-1f.
713          */
cubicnull714         fun cubic(x1: Float, y1: Float, x2: Float, y2: Float) = Easing("cubic($x1, $y1, $x2, $y2)")
715     }
716 }
717 
718 /** Determines a specific arc direction of the widget's path on a [Transition]. */
719 @ExperimentalMotionApi
720 class Arc internal constructor(val name: String) {
721     companion object {
722         val None = Arc("none")
723         val StartVertical = Arc("startVertical")
724         val StartHorizontal = Arc("startHorizontal")
725         val Flip = Arc("flip")
726         val Below = Arc("below")
727         val Above = Arc("above")
728     }
729 }
730 
731 /**
732  * Defines the type of motion used when animating during touch-up.
733  *
734  * @see velocity
735  * @see spring
736  */
737 @ExperimentalMotionApi
738 class SwipeMode
739 internal constructor(
740     val name: String,
741     internal val springMass: Float = 1f,
742     internal val springStiffness: Float = 400f,
743     internal val springDamping: Float = 10f,
744     internal val springThreshold: Float = 0.01f,
745     internal val springBoundary: SpringBoundary = SpringBoundary.Overshoot,
746     internal val maxVelocity: Float = 4f,
747     internal val maxAcceleration: Float = 1.2f
748 ) {
749     companion object {
750         /**
751          * The default Velocity based mode.
752          *
753          * Defined as `velocity(maxVelocity = 4f, maxAcceleration = 1.2f)`.
754          *
755          * @see velocity
756          */
757         val Velocity = velocity()
758 
759         /**
760          * The default Spring based mode.
761          *
762          * Defined as `spring(mass = 1f, stiffness = 400f, damping = 10f, threshold = 0.01f,
763          * boundary = SpringBoundary.Overshoot)`.
764          *
765          * @see spring
766          */
767         val Spring = spring()
768 
769         /**
770          * Velocity based behavior during touch up for [OnSwipe].
771          *
772          * @param maxVelocity Maximum velocity in pixels/milliSecond
773          * @param maxAcceleration Maximum acceleration in pixels/milliSecond^2
774          */
velocitynull775         fun velocity(maxVelocity: Float = 4f, maxAcceleration: Float = 1.2f): SwipeMode =
776             SwipeMode(
777                 name = "velocity",
778                 maxVelocity = maxVelocity,
779                 maxAcceleration = maxAcceleration
780             )
781 
782         /**
783          * Defines a spring based behavior during touch up for [OnSwipe].
784          *
785          * @param mass Mass of the spring, mostly affects the momentum that the spring carries. A
786          *   spring with a larger mass will overshoot more and take longer to settle.
787          * @param stiffness Stiffness of the spring, mostly affects the acceleration at the start of
788          *   the motion. A spring with higher stiffness will move faster when pulled at a constant
789          *   distance.
790          * @param damping The rate at which the spring settles on its final position. A spring with
791          *   larger damping value will settle faster on its final position.
792          * @param threshold Distance in meters from the target point at which the bouncing motion of
793          *   the spring is to be considered finished. 0.01 (1cm) by default. This value is typically
794          *   small since the widget will jump to the final position once the spring motion ends, a
795          *   large threshold value might cause the motion to end noticeably far from the target
796          *   point.
797          * @param boundary Behavior of the spring bouncing motion as it crosses its target position.
798          *   [SpringBoundary.Overshoot] by default.
799          */
800         fun spring(
801             mass: Float = 1f,
802             stiffness: Float = 400f,
803             damping: Float = 10f,
804             threshold: Float = 0.01f,
805             boundary: SpringBoundary = SpringBoundary.Overshoot
806         ): SwipeMode =
807             SwipeMode(
808                 name = "spring",
809                 springMass = mass,
810                 springStiffness = stiffness,
811                 springDamping = damping,
812                 springThreshold = threshold,
813                 springBoundary = boundary
814             )
815     }
816 }
817 
818 /**
819  * The logic used to decide the target position when the touch input ends.
820  *
821  * The possible target positions are the positions defined by the **start** and **end**
822  * [ConstraintSet]s.
823  *
824  * To define the type of motion used while animating during touch up, see [SwipeMode] for
825  * [OnSwipe.mode].
826  */
827 @ExperimentalMotionApi
828 class SwipeTouchUp internal constructor(val name: String) {
829     companion object {
830         /**
831          * The widget will be automatically animated towards the [ConstraintSet] closest to where
832          * the swipe motion is predicted to end.
833          */
834         val AutoComplete: SwipeTouchUp = SwipeTouchUp("autocomplete")
835 
836         /**
837          * Automatically animates towards the **start** [ConstraintSet] unless it's already exactly
838          * at the **end** [ConstraintSet].
839          *
840          * @see NeverCompleteEnd
841          */
842         val ToStart: SwipeTouchUp = SwipeTouchUp("toStart")
843 
844         /**
845          * Automatically animates towards the **end** [ConstraintSet] unless it's already exactly at
846          * the **start** [ConstraintSet].
847          *
848          * @see NeverCompleteStart
849          */
850         val ToEnd: SwipeTouchUp = SwipeTouchUp("toEnd")
851 
852         /** Stops right in place, will **not** automatically animate to any [ConstraintSet]. */
853         val Stop: SwipeTouchUp = SwipeTouchUp("stop")
854 
855         /**
856          * Automatically animates towards the point where the swipe motion is predicted to end.
857          *
858          * This is guaranteed to stop within the start or end [ConstraintSet]s in the case where
859          * it's carrying a lot of speed.
860          */
861         val Decelerate: SwipeTouchUp = SwipeTouchUp("decelerate")
862 
863         /**
864          * Similar to [ToEnd], but it will animate to the **end** [ConstraintSet] even if the widget
865          * is exactly at the start [ConstraintSet].
866          */
867         val NeverCompleteStart: SwipeTouchUp = SwipeTouchUp("neverCompleteStart")
868 
869         /**
870          * Similar to [ToStart], but it will animate to the **start** [ConstraintSet] even if the
871          * widget is exactly at the end [ConstraintSet].
872          */
873         val NeverCompleteEnd: SwipeTouchUp = SwipeTouchUp("neverCompleteEnd")
874     }
875 }
876 
877 /** Direction of the touch input that will initiate the swipe handling. */
878 @ExperimentalMotionApi
879 class SwipeDirection internal constructor(val name: String) {
880     companion object {
881         val Up: SwipeDirection = SwipeDirection("up")
882         val Down: SwipeDirection = SwipeDirection("down")
883         val Left: SwipeDirection = SwipeDirection("left")
884         val Right: SwipeDirection = SwipeDirection("right")
885         val Start: SwipeDirection = SwipeDirection("start")
886         val End: SwipeDirection = SwipeDirection("end")
887         val Clockwise: SwipeDirection = SwipeDirection("clockwise")
888         val Counterclockwise: SwipeDirection = SwipeDirection("anticlockwise")
889     }
890 }
891 
892 /**
893  * Side of the bounds to track during touch handling, this is to account for when the widget changes
894  * size during the [Transition].
895  */
896 @ExperimentalMotionApi
897 class SwipeSide internal constructor(val name: String) {
898     companion object {
899         val Top: SwipeSide = SwipeSide("top")
900         val Left: SwipeSide = SwipeSide("left")
901         val Right: SwipeSide = SwipeSide("right")
902         val Bottom: SwipeSide = SwipeSide("bottom")
903         val Middle: SwipeSide = SwipeSide("middle")
904         val Start: SwipeSide = SwipeSide("start")
905         val End: SwipeSide = SwipeSide("end")
906     }
907 }
908 
909 /**
910  * Behavior of the spring as it crosses its target position. The target position may be the start or
911  * end of the [Transition].
912  */
913 @ExperimentalMotionApi
914 class SpringBoundary internal constructor(val name: String) {
915     companion object {
916         /** The default Spring behavior, it will overshoot around the target position. */
917         val Overshoot = SpringBoundary("overshoot")
918 
919         /**
920          * Bouncing motion when the target position is at the start of the [Transition]. Otherwise,
921          * it will overshoot.
922          */
923         val BounceStart = SpringBoundary("bounceStart")
924 
925         /**
926          * Bouncing motion when the target position is at the end of the [Transition]. Otherwise, it
927          * will overshoot.
928          */
929         val BounceEnd = SpringBoundary("bounceEnd")
930 
931         /**
932          * Bouncing motion whenever it crosses the target position. This basically guarantees that
933          * the spring motion will never overshoot.
934          */
935         val BounceBoth = SpringBoundary("bounceBoth")
936     }
937 }
938 
939 /** Type of fit applied between curves. */
940 @ExperimentalMotionApi
941 class CurveFit internal constructor(override val name: String) : NamedPropertyOrValue {
942     companion object {
943         val Spline: CurveFit = CurveFit("spline")
944         val Linear: CurveFit = CurveFit("linear")
945     }
946 }
947 
948 /** Relative coordinate space in which KeyPositions are applied. */
949 @ExperimentalMotionApi
950 class RelativePosition internal constructor(override val name: String) : NamedPropertyOrValue {
951     companion object {
952         /**
953          * The default coordinate space, defined between the ending and starting point of the
954          * motion. Aligned to the layout's X and Y axis.
955          */
956         val Delta: RelativePosition = RelativePosition("deltaRelative")
957 
958         /**
959          * The coordinate space defined between the ending and starting point of the motion. Aligned
960          * perpendicularly to the shortest line between the start/end.
961          */
962         val Path: RelativePosition = RelativePosition("pathRelative")
963 
964         /**
965          * The coordinate space defined within the parent layout bounds (the MotionLayout parent).
966          */
967         val Parent: RelativePosition = RelativePosition("parentRelative")
968     }
969 }
970