• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.activity.compose.BackHandler
20 import androidx.compose.foundation.gestures.Orientation
21 import androidx.compose.foundation.layout.Box
22 import androidx.compose.runtime.Composable
23 import androidx.compose.runtime.DisposableEffect
24 import androidx.compose.runtime.LaunchedEffect
25 import androidx.compose.runtime.SideEffect
26 import androidx.compose.runtime.getValue
27 import androidx.compose.runtime.key
28 import androidx.compose.runtime.mutableStateOf
29 import androidx.compose.runtime.remember
30 import androidx.compose.runtime.setValue
31 import androidx.compose.runtime.snapshots.SnapshotStateMap
32 import androidx.compose.ui.ExperimentalComposeUiApi
33 import androidx.compose.ui.Modifier
34 import androidx.compose.ui.draw.drawWithContent
35 import androidx.compose.ui.layout.LookaheadScope
36 import androidx.compose.ui.layout.onSizeChanged
37 import androidx.compose.ui.unit.Density
38 import androidx.compose.ui.unit.IntSize
39 import com.android.compose.ui.util.fastForEach
40 import kotlinx.coroutines.channels.Channel
41 
42 internal class SceneTransitionLayoutImpl(
43     onChangeScene: (SceneKey) -> Unit,
44     builder: SceneTransitionLayoutScope.() -> Unit,
45     transitions: SceneTransitions,
46     internal val state: SceneTransitionLayoutState,
47     density: Density,
48 ) {
49     internal val scenes = SnapshotStateMap<SceneKey, Scene>()
50     internal val elements = SnapshotStateMap<ElementKey, Element>()
51 
52     /** The scenes that are "ready", i.e. they were composed and fully laid-out at least once. */
53     private val readyScenes = SnapshotStateMap<SceneKey, Boolean>()
54 
55     internal var onChangeScene by mutableStateOf(onChangeScene)
56     internal var transitions by mutableStateOf(transitions)
57     internal var density: Density by mutableStateOf(density)
58 
59     /**
60      * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
61      * any scene configured or right before the first measure pass of the layout.
62      */
63     internal var size by mutableStateOf(IntSize.Zero)
64 
65     init {
66         setScenes(builder)
67     }
68 
69     internal fun scene(key: SceneKey): Scene {
70         return scenes[key] ?: error("Scene $key is not configured")
71     }
72 
73     internal fun setScenes(builder: SceneTransitionLayoutScope.() -> Unit) {
74         // Keep a reference of the current scenes. After processing [builder], the scenes that were
75         // not configured will be removed.
76         val scenesToRemove = scenes.keys.toMutableSet()
77 
78         // The incrementing zIndex of each scene.
79         var zIndex = 0f
80 
81         object : SceneTransitionLayoutScope {
82                 override fun scene(
83                     key: SceneKey,
84                     userActions: Map<UserAction, SceneKey>,
85                     content: @Composable SceneScope.() -> Unit,
86                 ) {
87                     scenesToRemove.remove(key)
88 
89                     val scene = scenes[key]
90                     if (scene != null) {
91                         // Update an existing scene.
92                         scene.content = content
93                         scene.userActions = userActions
94                         scene.zIndex = zIndex
95                     } else {
96                         // New scene.
97                         scenes[key] =
98                             Scene(
99                                 key,
100                                 this@SceneTransitionLayoutImpl,
101                                 content,
102                                 userActions,
103                                 zIndex,
104                             )
105                     }
106 
107                     zIndex++
108                 }
109             }
110             .builder()
111 
112         scenesToRemove.forEach { scenes.remove(it) }
113     }
114 
115     @Composable
116     internal fun setCurrentScene(key: SceneKey) {
117         val channel = remember { Channel<SceneKey>(Channel.CONFLATED) }
118         SideEffect { channel.trySend(key) }
119         LaunchedEffect(channel) {
120             for (newKey in channel) {
121                 // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame
122                 // late.
123                 val newKey = channel.tryReceive().getOrNull() ?: newKey
124                 animateToScene(this@SceneTransitionLayoutImpl, newKey)
125             }
126         }
127     }
128 
129     @Composable
130     @OptIn(ExperimentalComposeUiApi::class)
131     internal fun Content(modifier: Modifier) {
132         Box(
133             modifier
134                 // Handle horizontal and vertical swipes on this layout.
135                 // Note: order here is important and will give a slight priority to the vertical
136                 // swipes.
137                 .swipeToScene(layoutImpl = this, Orientation.Horizontal)
138                 .swipeToScene(layoutImpl = this, Orientation.Vertical)
139                 .onSizeChanged { size = it }
140         ) {
141             LookaheadScope {
142                 val scenesToCompose =
143                     when (val state = state.transitionState) {
144                         is TransitionState.Idle -> listOf(scene(state.currentScene))
145                         is TransitionState.Transition -> {
146                             if (state.toScene != state.fromScene) {
147                                 listOf(scene(state.toScene), scene(state.fromScene))
148                             } else {
149                                 listOf(scene(state.fromScene))
150                             }
151                         }
152                     }
153 
154                 // Handle back events.
155                 // TODO(b/290184746): Make sure that this works with SystemUI once we use
156                 // SceneTransitionLayout in Flexiglass.
157                 scene(state.transitionState.currentScene).userActions[Back]?.let { backScene ->
158                     BackHandler { onChangeScene(backScene) }
159                 }
160 
161                 Box(
162                     Modifier.drawWithContent {
163                         drawContent()
164 
165                         // At this point, all scenes in scenesToCompose are fully laid out so they
166                         // are marked as ready. This is necessary because the animation code needs
167                         // to know the position and size of the elements in each scenes they are in,
168                         // so [readyScenes] will be used to decide whether the transition is ready
169                         // (see isTransitionReady() below).
170                         //
171                         // We can't do that in a DisposableEffect or SideEffect because those are
172                         // run between composition and layout. LaunchedEffect could work and might
173                         // be better, but it looks like launched effects run a frame later than this
174                         // code so doing this here seems better for performance.
175                         scenesToCompose.fastForEach { readyScenes[it.key] = true }
176                     }
177                 ) {
178                     scenesToCompose.fastForEach { scene ->
179                         val key = scene.key
180                         key(key) {
181                             DisposableEffect(key) { onDispose { readyScenes.remove(key) } }
182 
183                             scene.Content(
184                                 Modifier.drawWithContent {
185                                     when (val state = state.transitionState) {
186                                         is TransitionState.Idle -> drawContent()
187                                         is TransitionState.Transition -> {
188                                             // Don't draw scenes that are not ready yet.
189                                             if (
190                                                 readyScenes.containsKey(key) ||
191                                                     state.fromScene == state.toScene
192                                             ) {
193                                                 drawContent()
194                                             }
195                                         }
196                                     }
197                                 }
198                             )
199                         }
200                     }
201                 }
202             }
203         }
204     }
205 
206     /**
207      * Return whether [transition] is ready, i.e. the elements of both scenes of the transition were
208      * laid out at least once.
209      */
210     internal fun isTransitionReady(transition: TransitionState.Transition): Boolean {
211         return readyScenes.containsKey(transition.fromScene) &&
212             readyScenes.containsKey(transition.toScene)
213     }
214 }
215