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