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