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.transformation 18 19 import androidx.compose.ui.Modifier 20 import com.android.compose.animation.scene.Element 21 import com.android.compose.animation.scene.ElementMatcher 22 import com.android.compose.animation.scene.Scene 23 import com.android.compose.animation.scene.SceneTransitionLayoutImpl 24 import com.android.compose.animation.scene.TransitionState 25 26 /** A transformation applied to one or more elements during a transition. */ 27 sealed interface Transformation { 28 /** 29 * The matcher that should match the element(s) to which this transformation should be applied. 30 */ 31 val matcher: ElementMatcher 32 33 /* 34 * Reverse this transformation. This is called when we use Transition(from = A, to = B) when 35 * animating from B to A and there is no Transition(from = B, to = A) defined. 36 */ 37 fun reverse(): Transformation = this 38 } 39 40 /** A transformation that is applied on the element during the whole transition. */ 41 internal interface ModifierTransformation : Transformation { 42 /** Apply the transformation to [element]. */ 43 // TODO(b/290184746): Figure out a public API for custom transformations that don't have access 44 // to these internal classes. 45 fun Modifier.transform( 46 layoutImpl: SceneTransitionLayoutImpl, 47 scene: Scene, 48 element: Element, 49 sceneValues: Element.SceneValues, 50 ): Modifier 51 } 52 53 /** A transformation that changes the value of an element property, like its size or offset. */ 54 internal sealed interface PropertyTransformation<T> : Transformation { 55 /** 56 * The range during which the transformation is applied. If it is `null`, then the 57 * transformation will be applied throughout the whole scene transition. 58 */ 59 val range: TransformationRange? 60 get() = null 61 62 /** 63 * Transform [value], i.e. the value of the transformed property without this transformation. 64 */ 65 // TODO(b/290184746): Figure out a public API for custom transformations that don't have access 66 // to these internal classes. 67 fun transform( 68 layoutImpl: SceneTransitionLayoutImpl, 69 scene: Scene, 70 element: Element, 71 sceneValues: Element.SceneValues, 72 transition: TransitionState.Transition, 73 value: T, 74 ): T 75 } 76 77 /** 78 * A [PropertyTransformation] associated to a range. This is a helper class so that normal 79 * implementations of [PropertyTransformation] don't have to take care of reversing their range when 80 * they are reversed. 81 */ 82 internal class RangedPropertyTransformation<T>( 83 val delegate: PropertyTransformation<T>, 84 override val range: TransformationRange, 85 ) : PropertyTransformation<T> by delegate { 86 override fun reverse(): Transformation { 87 return RangedPropertyTransformation( 88 delegate.reverse() as PropertyTransformation<T>, 89 range.reverse() 90 ) 91 } 92 } 93 94 /** The progress-based range of a [PropertyTransformation]. */ 95 data class TransformationRange 96 private constructor( 97 val start: Float, 98 val end: Float, 99 ) { 100 constructor( 101 start: Float? = null, 102 end: Float? = null 103 ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified) 104 105 init { 106 require(!start.isSpecified() || (start in 0f..1f)) 107 require(!end.isSpecified() || (end in 0f..1f)) 108 require(!start.isSpecified() || !end.isSpecified() || start <= end) 109 } 110 111 /** Reverse this range. */ 112 fun reverse() = TransformationRange(start = reverseBound(end), end = reverseBound(start)) 113 114 /** Get the progress of this range given the global [transitionProgress]. */ 115 fun progress(transitionProgress: Float): Float { 116 return when { 117 start.isSpecified() && end.isSpecified() -> 118 ((transitionProgress - start) / (end - start)).coerceIn(0f, 1f) 119 !start.isSpecified() && !end.isSpecified() -> transitionProgress 120 end.isSpecified() -> (transitionProgress / end).coerceAtMost(1f) 121 else -> ((transitionProgress - start) / (1f - start)).coerceAtLeast(0f) 122 } 123 } 124 125 private fun Float.isSpecified() = this != BoundUnspecified 126 127 private fun reverseBound(bound: Float): Float { 128 return if (bound.isSpecified()) { 129 1f - bound 130 } else { 131 BoundUnspecified 132 } 133 } 134 135 companion object { 136 private const val BoundUnspecified = Float.MIN_VALUE 137 } 138 } 139