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.animation.core.snap 21 import androidx.compose.ui.geometry.Offset 22 import androidx.compose.ui.unit.IntSize 23 import com.android.compose.animation.scene.transformation.AnchoredSize 24 import com.android.compose.animation.scene.transformation.AnchoredTranslate 25 import com.android.compose.animation.scene.transformation.EdgeTranslate 26 import com.android.compose.animation.scene.transformation.Fade 27 import com.android.compose.animation.scene.transformation.ModifierTransformation 28 import com.android.compose.animation.scene.transformation.PropertyTransformation 29 import com.android.compose.animation.scene.transformation.RangedPropertyTransformation 30 import com.android.compose.animation.scene.transformation.ScaleSize 31 import com.android.compose.animation.scene.transformation.Transformation 32 import com.android.compose.animation.scene.transformation.Translate 33 import com.android.compose.ui.util.fastForEach 34 import com.android.compose.ui.util.fastMap 35 36 /** The transitions configuration of a [SceneTransitionLayout]. */ 37 class SceneTransitions( 38 private val transitionSpecs: List<TransitionSpec>, 39 ) { 40 private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>() 41 42 internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec { 43 return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) } 44 } 45 46 private fun findSpec(from: SceneKey, to: SceneKey): TransitionSpec { 47 val spec = transition(from, to) { it.from == from && it.to == to } 48 if (spec != null) { 49 return spec 50 } 51 52 val reversed = transition(from, to) { it.from == to && it.to == from } 53 if (reversed != null) { 54 return reversed.reverse() 55 } 56 57 val relaxedSpec = 58 transition(from, to) { 59 (it.from == from && it.to == null) || (it.to == to && it.from == null) 60 } 61 if (relaxedSpec != null) { 62 return relaxedSpec 63 } 64 65 return transition(from, to) { 66 (it.from == to && it.to == null) || (it.to == from && it.from == null) 67 } 68 ?.reverse() 69 ?: defaultTransition(from, to) 70 } 71 72 private fun transition( 73 from: SceneKey, 74 to: SceneKey, 75 filter: (TransitionSpec) -> Boolean, 76 ): TransitionSpec? { 77 var match: TransitionSpec? = null 78 transitionSpecs.fastForEach { spec -> 79 if (filter(spec)) { 80 if (match != null) { 81 error("Found multiple transition specs for transition $from => $to") 82 } 83 match = spec 84 } 85 } 86 return match 87 } 88 89 private fun defaultTransition(from: SceneKey, to: SceneKey) = 90 TransitionSpec(from, to, emptyList(), snap()) 91 } 92 93 /** The definition of a transition between [from] and [to]. */ 94 data class TransitionSpec( 95 val from: SceneKey?, 96 val to: SceneKey?, 97 val transformations: List<Transformation>, 98 val spec: AnimationSpec<Float>, 99 ) { 100 private val cache = mutableMapOf<ElementKey, ElementTransformations>() 101 102 internal fun reverse(): TransitionSpec { 103 return copy( 104 from = to, 105 to = from, 106 transformations = transformations.fastMap { it.reverse() }, 107 ) 108 } 109 110 internal fun transformations(element: ElementKey): ElementTransformations { 111 return cache.getOrPut(element) { computeTransformations(element) } 112 } 113 114 /** Filter [transformations] to compute the [ElementTransformations] of [element]. */ 115 private fun computeTransformations(element: ElementKey): ElementTransformations { 116 val modifier = mutableListOf<ModifierTransformation>() 117 var offset: PropertyTransformation<Offset>? = null 118 var size: PropertyTransformation<IntSize>? = null 119 var alpha: PropertyTransformation<Float>? = null 120 121 fun <T> onPropertyTransformation( 122 root: PropertyTransformation<T>, 123 current: PropertyTransformation<T> = root, 124 ) { 125 when (current) { 126 is Translate, 127 is EdgeTranslate, 128 is AnchoredTranslate -> { 129 throwIfNotNull(offset, element, property = "offset") 130 offset = root as PropertyTransformation<Offset> 131 } 132 is ScaleSize, 133 is AnchoredSize -> { 134 throwIfNotNull(size, element, property = "size") 135 size = root as PropertyTransformation<IntSize> 136 } 137 is Fade -> { 138 throwIfNotNull(alpha, element, property = "alpha") 139 alpha = root as PropertyTransformation<Float> 140 } 141 is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate) 142 } 143 } 144 145 transformations.fastForEach { transformation -> 146 if (!transformation.matcher.matches(element)) { 147 return@fastForEach 148 } 149 150 when (transformation) { 151 is ModifierTransformation -> modifier.add(transformation) 152 is PropertyTransformation<*> -> onPropertyTransformation(transformation) 153 } 154 } 155 156 return ElementTransformations(modifier, offset, size, alpha) 157 } 158 159 private fun throwIfNotNull( 160 previous: PropertyTransformation<*>?, 161 element: ElementKey, 162 property: String, 163 ) { 164 if (previous != null) { 165 error("$element has multiple transformations for its $property property") 166 } 167 } 168 } 169 170 /** The transformations of an element during a transition. */ 171 internal class ElementTransformations( 172 val modifier: List<ModifierTransformation>, 173 val offset: PropertyTransformation<Offset>?, 174 val size: PropertyTransformation<IntSize>?, 175 val alpha: PropertyTransformation<Float>?, 176 ) 177