1 /* 2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.compose.animation.scene 18 19 import androidx.compose.animation.core.AnimationSpec 20 import androidx.compose.ui.graphics.RectangleShape 21 import androidx.compose.ui.graphics.Shape 22 import androidx.compose.ui.unit.Dp 23 import androidx.compose.ui.unit.dp 24 25 /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */ 26 fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions { 27 return transitionsImpl(builder) 28 } 29 30 @DslMarker annotation class TransitionDsl 31 32 @TransitionDsl 33 interface SceneTransitionsBuilder { 34 /** 35 * Define the default animation to be played when transitioning [to] the specified scene, from 36 * any scene. For the animation specification to apply only when transitioning between two 37 * specific scenes, use [from] instead. 38 * 39 * @see from 40 */ 41 fun to( 42 to: SceneKey, 43 builder: TransitionBuilder.() -> Unit = {}, 44 ): TransitionSpec 45 46 /** 47 * Define the animation to be played when transitioning [from] the specified scene. For the 48 * animation specification to apply only when transitioning between two specific scenes, pass 49 * the destination scene via the [to] argument. 50 * 51 * When looking up which transition should be used when animating from scene A to scene B, we 52 * pick the single transition matching one of these predicates (in order of importance): 53 * 1. from == A && to == B 54 * 2. to == A && from == B, which is then treated in reverse. 55 * 3. (from == A && to == null) || (from == null && to == B) 56 * 4. (from == B && to == null) || (from == null && to == A), which is then treated in reverse. 57 */ 58 fun from( 59 from: SceneKey, 60 to: SceneKey? = null, 61 builder: TransitionBuilder.() -> Unit = {}, 62 ): TransitionSpec 63 } 64 65 @TransitionDsl 66 interface TransitionBuilder : PropertyTransformationBuilder { 67 /** 68 * The [AnimationSpec] used to animate the progress of this transition from `0` to `1` when 69 * performing programmatic (not input pointer tracking) animations. 70 */ 71 var spec: AnimationSpec<Float> 72 73 /** 74 * Define a progress-based range for the transformations inside [builder]. 75 * 76 * For instance, the following will fade `Foo` during the first half of the transition then it 77 * will translate it by 100.dp during the second half. 78 * 79 * ``` 80 * fractionRange(end = 0.5f) { fade(Foo) } 81 * fractionRange(start = 0.5f) { translate(Foo, x = 100.dp) } 82 * ``` 83 * 84 * @param start the start of the range, in the [0; 1] range. 85 * @param end the end of the range, in the [0; 1] range. 86 */ 87 fun fractionRange( 88 start: Float? = null, 89 end: Float? = null, 90 builder: PropertyTransformationBuilder.() -> Unit, 91 ) 92 93 /** 94 * Define a timestamp-based range for the transformations inside [builder]. 95 * 96 * For instance, the following will fade `Foo` during the first half of the transition then it 97 * will translate it by 100.dp during the second half. 98 * 99 * ``` 100 * spec = tween(500) 101 * timestampRange(end = 250) { fade(Foo) } 102 * timestampRange(start = 250) { translate(Foo, x = 100.dp) } 103 * ``` 104 * 105 * Important: [spec] must be a [androidx.compose.animation.core.DurationBasedAnimationSpec] if 106 * you call [timestampRange], otherwise this will throw. The spec duration will be used to 107 * transform this range into a [fractionRange]. 108 * 109 * @param startMillis the start of the range, in the [0; spec.duration] range. 110 * @param endMillis the end of the range, in the [0; spec.duration] range. 111 */ 112 fun timestampRange( 113 startMillis: Int? = null, 114 endMillis: Int? = null, 115 builder: PropertyTransformationBuilder.() -> Unit, 116 ) 117 118 /** 119 * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and 120 * using the given [shape]. 121 * 122 * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area. 123 * This can be used to make content drawn below an opaque element visible. For example, if we 124 * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below 125 * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big 126 * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be 127 * the result. 128 */ 129 fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape = RectangleShape) 130 } 131 132 @TransitionDsl 133 interface PropertyTransformationBuilder { 134 /** 135 * Fade the element(s) matching [matcher]. This will automatically fade in or fade out if the 136 * element is entering or leaving the scene, respectively. 137 */ 138 fun fade(matcher: ElementMatcher) 139 140 /** Translate the element(s) matching [matcher] by ([x], [y]) dp. */ 141 fun translate(matcher: ElementMatcher, x: Dp = 0.dp, y: Dp = 0.dp) 142 143 /** 144 * Translate the element(s) matching [matcher] from/to the [edge] of the [SceneTransitionLayout] 145 * animating it. 146 * 147 * If [startsOutsideLayoutBounds] is `true`, then the element will start completely outside of 148 * the layout bounds (i.e. none of it will be visible at progress = 0f if the layout clips its 149 * content). If it is `false`, then the element will start aligned with the edge of the layout 150 * (i.e. it will be completely visible at progress = 0f). 151 */ 152 fun translate(matcher: ElementMatcher, edge: Edge, startsOutsideLayoutBounds: Boolean = true) 153 154 /** 155 * Translate the element(s) matching [matcher] by the same amount that [anchor] is translated 156 * during this transition. 157 * 158 * Note: This currently only works if [anchor] is a shared element of this transition. 159 * 160 * TODO(b/290184746): Also support anchors that are not shared but translated because of other 161 * transformations, like an edge translation. 162 */ 163 fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey) 164 165 /** 166 * Scale the [width] and [height] of the element(s) matching [matcher]. Note that this scaling 167 * is done during layout, so it will potentially impact the size and position of other elements. 168 * 169 * TODO(b/290184746): Also provide a scaleDrawing() to scale an element at drawing time. 170 */ 171 fun scaleSize(matcher: ElementMatcher, width: Float = 1f, height: Float = 1f) 172 173 /** 174 * Scale the element(s) matching [matcher] so that it grows/shrinks to the same size as [anchor] 175 * . 176 * 177 * Note: This currently only works if [anchor] is a shared element of this transition. 178 */ 179 fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey) 180 } 181 182 /** An interface to match one or more elements. */ 183 interface ElementMatcher { 184 /** Whether the element with key [key] matches this matcher. */ 185 fun matches(key: ElementKey): Boolean 186 } 187 188 /** The edge of a [SceneTransitionLayout]. */ 189 enum class Edge { 190 Left, 191 Right, 192 Top, 193 Bottom, 194 } 195