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