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.geometry.Offset
20 import androidx.compose.ui.geometry.isSpecified
21 import com.android.compose.animation.scene.Element
22 import com.android.compose.animation.scene.ElementKey
23 import com.android.compose.animation.scene.ElementMatcher
24 import com.android.compose.animation.scene.SceneKey
25 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
26 import com.android.compose.animation.scene.TransitionState
27 
28 /** Anchor the translation of an element to another element. */
29 internal class AnchoredTranslate(
30     override val matcher: ElementMatcher,
31     private val anchor: ElementKey,
32 ) : PropertyTransformation<Offset> {
transformnull33     override fun transform(
34         layoutImpl: SceneTransitionLayoutImpl,
35         scene: SceneKey,
36         element: Element,
37         sceneState: Element.SceneState,
38         transition: TransitionState.Transition,
39         value: Offset,
40     ): Offset {
41         fun throwException(scene: SceneKey?): Nothing {
42             throwMissingAnchorException(
43                 transformation = "AnchoredTranslate",
44                 anchor = anchor,
45                 scene = scene,
46             )
47         }
48 
49         val anchor = layoutImpl.elements[anchor] ?: throwException(scene = null)
50         fun anchorOffsetIn(scene: SceneKey): Offset? {
51             return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified }
52         }
53 
54         // [element] will move the same amount as [anchor] does.
55         // TODO(b/290184746): Also support anchors that are not shared but translated because of
56         // other transformations, like an edge translation.
57         val anchorFromOffset =
58             anchorOffsetIn(transition.fromScene) ?: throwException(transition.fromScene)
59         val anchorToOffset =
60             anchorOffsetIn(transition.toScene) ?: throwException(transition.toScene)
61         val offset = anchorToOffset - anchorFromOffset
62 
63         return if (scene == transition.toScene) {
64             Offset(
65                 value.x - offset.x,
66                 value.y - offset.y,
67             )
68         } else {
69             Offset(
70                 value.x + offset.x,
71                 value.y + offset.y,
72             )
73         }
74     }
75 }
76 
throwMissingAnchorExceptionnull77 internal fun throwMissingAnchorException(
78     transformation: String,
79     anchor: ElementKey,
80     scene: SceneKey?,
81 ): Nothing {
82     error(
83         """
84         Anchor ${anchor.debugName} does not have a target state in scene ${scene?.debugName}.
85         This either means that it was not composed at all during the transition or that it was
86         composed too late, for instance during layout/subcomposition. To avoid flickers in
87         $transformation, you should make sure that the composition and layout of anchor is *not*
88         deferred, for instance by moving it out of lazy layouts.
89     """
90             .trimIndent()
91     )
92 }
93