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.runtime.Composable
20 import androidx.compose.runtime.State
21 import androidx.compose.runtime.remember
22 import androidx.compose.ui.Modifier
23 import androidx.compose.ui.platform.LocalDensity
24 
25 /**
26  * [SceneTransitionLayout] is a container that automatically animates its content whenever
27  * [currentScene] changes, using the transitions defined in [transitions].
28  *
29  * Note: You should use [androidx.compose.animation.AnimatedContent] instead of
30  * [SceneTransitionLayout] if it fits your need. Use [SceneTransitionLayout] over AnimatedContent if
31  * you need support for swipe gestures, shared elements or transitions defined declaratively outside
32  * UI code.
33  *
34  * @param currentScene the current scene
35  * @param onChangeScene a mutator that should set [currentScene] to the given scene when called.
36  *   This is called when the user commits a transition to a new scene because of a [UserAction], for
37  *   instance by triggering back navigation or by swiping to a new scene.
38  * @param transitions the definition of the transitions used to animate a change of scene.
39  * @param state the observable state of this layout.
40  * @param scenes the configuration of the different scenes of this layout.
41  */
42 @Composable
43 fun SceneTransitionLayout(
44     currentScene: SceneKey,
45     onChangeScene: (SceneKey) -> Unit,
46     transitions: SceneTransitions,
47     modifier: Modifier = Modifier,
48     state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
49     scenes: SceneTransitionLayoutScope.() -> Unit,
50 ) {
51     val density = LocalDensity.current
52     val layoutImpl = remember {
53         SceneTransitionLayoutImpl(
54             onChangeScene,
55             scenes,
56             transitions,
57             state,
58             density,
59         )
60     }
61 
62     layoutImpl.onChangeScene = onChangeScene
63     layoutImpl.transitions = transitions
64     layoutImpl.density = density
65     layoutImpl.setScenes(scenes)
66     layoutImpl.setCurrentScene(currentScene)
67 
68     layoutImpl.Content(modifier)
69 }
70 
71 interface SceneTransitionLayoutScope {
72     /**
73      * Add a scene to this layout, identified by [key].
74      *
75      * You can configure [userActions] so that swiping on this layout or navigating back will
76      * transition to a different scene.
77      *
78      * Important: scene order along the z-axis follows call order. Calling scene(A) followed by
79      * scene(B) will mean that scene B renders after/above scene A.
80      */
81     fun scene(
82         key: SceneKey,
83         userActions: Map<UserAction, SceneKey> = emptyMap(),
84         content: @Composable SceneScope.() -> Unit,
85     )
86 }
87 
88 interface SceneScope {
89     /**
90      * Tag an element identified by [key].
91      *
92      * Tagging an element will allow you to reference that element when defining transitions, so
93      * that the element can be transformed and animated when the scene transitions in or out.
94      *
95      * Additionally, this [key] will be used to detect elements that are shared between scenes to
96      * automatically interpolate their size, offset and [shared values][animateSharedValueAsState].
97      *
98      * TODO(b/291566282): Migrate this to the new Modifier Node API and remove the @Composable
99      *   constraint.
100      */
101     @Composable fun Modifier.element(key: ElementKey): Modifier
102 
103     /**
104      * Animate some value of a shared element.
105      *
106      * @param value the value of this shared value in the current scene.
107      * @param key the key of this shared value.
108      * @param element the element associated with this value.
109      * @param lerp the *linear* interpolation function that should be used to interpolate between
110      *   two different values. Note that it has to be linear because the [fraction] passed to this
111      *   interpolator is already interpolated.
112      * @param canOverflow whether this value can overflow past the values it is interpolated
113      *   between, for instance because the transition is animated using a bouncy spring.
114      * @see animateSharedIntAsState
115      * @see animateSharedFloatAsState
116      * @see animateSharedDpAsState
117      * @see animateSharedColorAsState
118      */
119     @Composable
120     fun <T> animateSharedValueAsState(
121         value: T,
122         key: ValueKey,
123         element: ElementKey,
124         lerp: (start: T, stop: T, fraction: Float) -> T,
125         canOverflow: Boolean,
126     ): State<T>
127 }
128 
129 /** An action performed by the user. */
130 sealed interface UserAction
131 
132 /** The user navigated back, either using a gesture or by triggering a KEYCODE_BACK event. */
133 object Back : UserAction
134 
135 /** The user swiped on the container. */
136 enum class Swipe : UserAction {
137     Up,
138     Down,
139     Left,
140     Right,
141 }
142