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.Stable 24 import androidx.compose.runtime.key 25 import androidx.compose.runtime.snapshots.SnapshotStateMap 26 import androidx.compose.ui.ExperimentalComposeUiApi 27 import androidx.compose.ui.Modifier 28 import androidx.compose.ui.layout.ApproachLayoutModifierNode 29 import androidx.compose.ui.layout.ApproachMeasureScope 30 import androidx.compose.ui.layout.LookaheadScope 31 import androidx.compose.ui.layout.Measurable 32 import androidx.compose.ui.layout.MeasureResult 33 import androidx.compose.ui.node.ModifierNodeElement 34 import androidx.compose.ui.unit.Constraints 35 import androidx.compose.ui.unit.Density 36 import androidx.compose.ui.unit.IntSize 37 import androidx.compose.ui.util.fastForEach 38 import androidx.compose.ui.util.fastForEachReversed 39 import com.android.compose.ui.util.lerp 40 import kotlinx.coroutines.CoroutineScope 41 42 /** The type for the content of movable elements. */ 43 internal typealias MovableElementContent = @Composable (@Composable () -> Unit) -> Unit 44 45 @Stable 46 internal class SceneTransitionLayoutImpl( 47 internal val state: BaseSceneTransitionLayoutState, 48 internal var density: Density, 49 internal var swipeSourceDetector: SwipeSourceDetector, 50 internal var transitionInterceptionThreshold: Float, 51 builder: SceneTransitionLayoutScope.() -> Unit, 52 internal val coroutineScope: CoroutineScope, 53 ) { 54 /** 55 * The map of [Scene]s. 56 * 57 * TODO(b/317014852): Make this a normal MutableMap instead. 58 */ 59 internal val scenes = SnapshotStateMap<SceneKey, Scene>() 60 61 /** 62 * The map of [Element]s. 63 * 64 * Important: [Element]s from this map should never be accessed during composition because the 65 * Elements are added when the associated Modifier.element() node is attached to the Modifier 66 * tree, i.e. after composition. 67 */ 68 internal val elements = mutableMapOf<ElementKey, Element>() 69 70 /** 71 * The map of contents of movable elements. 72 * 73 * Note that given that this map is mutated directly during a composition, it has to be a 74 * [SnapshotStateMap] to make sure that mutations are reverted if composition is cancelled. 75 */ 76 private var _movableContents: SnapshotStateMap<ElementKey, MovableElementContent>? = null 77 val movableContents: SnapshotStateMap<ElementKey, MovableElementContent> 78 get() = 79 _movableContents 80 ?: SnapshotStateMap<ElementKey, MovableElementContent>().also { 81 _movableContents = it 82 } 83 84 /** 85 * The different values of a shared value keyed by a a [ValueKey] and the different elements and 86 * scenes it is associated to. 87 */ 88 private var _sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>? = 89 null 90 internal val sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>> 91 get() = 92 _sharedValues 93 ?: mutableMapOf<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>().also { 94 _sharedValues = it 95 } 96 97 // TODO(b/317958526): Lazily allocate scene gesture handlers the first time they are needed. 98 private val horizontalDraggableHandler: DraggableHandlerImpl 99 private val verticalDraggableHandler: DraggableHandlerImpl 100 101 internal val elementStateScope = ElementStateScopeImpl(this) 102 private var _userActionDistanceScope: UserActionDistanceScope? = null 103 internal val userActionDistanceScope: UserActionDistanceScope 104 get() = 105 _userActionDistanceScope 106 ?: UserActionDistanceScopeImpl(layoutImpl = this).also { 107 _userActionDistanceScope = it 108 } 109 110 /** 111 * The [LookaheadScope] of this layout, that can be used to compute offsets relative to the 112 * layout. 113 */ 114 internal lateinit var lookaheadScope: LookaheadScope 115 private set 116 117 init { 118 updateScenes(builder) 119 120 // DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the 121 // current scene (required for SwipeTransition). 122 horizontalDraggableHandler = 123 DraggableHandlerImpl( 124 layoutImpl = this, 125 orientation = Orientation.Horizontal, 126 coroutineScope = coroutineScope, 127 ) 128 129 verticalDraggableHandler = 130 DraggableHandlerImpl( 131 layoutImpl = this, 132 orientation = Orientation.Vertical, 133 coroutineScope = coroutineScope, 134 ) 135 136 // Make sure that the state is created on the same thread (most probably the main thread) 137 // than this STLImpl. 138 state.checkThread() 139 } 140 141 internal fun draggableHandler(orientation: Orientation): DraggableHandlerImpl = 142 when (orientation) { 143 Orientation.Vertical -> verticalDraggableHandler 144 Orientation.Horizontal -> horizontalDraggableHandler 145 } 146 147 internal fun scene(key: SceneKey): Scene { 148 return scenes[key] ?: error("Scene $key is not configured") 149 } 150 151 internal fun updateScenes(builder: SceneTransitionLayoutScope.() -> Unit) { 152 // Keep a reference of the current scenes. After processing [builder], the scenes that were 153 // not configured will be removed. 154 val scenesToRemove = scenes.keys.toMutableSet() 155 156 // The incrementing zIndex of each scene. 157 var zIndex = 0f 158 159 object : SceneTransitionLayoutScope { 160 override fun scene( 161 key: SceneKey, 162 userActions: Map<UserAction, UserActionResult>, 163 content: @Composable SceneScope.() -> Unit, 164 ) { 165 scenesToRemove.remove(key) 166 167 val scene = scenes[key] 168 if (scene != null) { 169 // Update an existing scene. 170 scene.content = content 171 scene.userActions = userActions 172 scene.zIndex = zIndex 173 } else { 174 // New scene. 175 scenes[key] = 176 Scene( 177 key, 178 this@SceneTransitionLayoutImpl, 179 content, 180 userActions, 181 zIndex, 182 ) 183 } 184 185 zIndex++ 186 } 187 } 188 .builder() 189 190 scenesToRemove.forEach { scenes.remove(it) } 191 } 192 193 @Composable 194 internal fun Content(modifier: Modifier, swipeDetector: SwipeDetector) { 195 Box( 196 modifier 197 // Handle horizontal and vertical swipes on this layout. 198 // Note: order here is important and will give a slight priority to the vertical 199 // swipes. 200 .swipeToScene(horizontalDraggableHandler, swipeDetector) 201 .swipeToScene(verticalDraggableHandler, swipeDetector) 202 .then(LayoutElement(layoutImpl = this)) 203 ) { 204 LookaheadScope { 205 lookaheadScope = this 206 207 BackHandler() 208 209 scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } } 210 } 211 } 212 } 213 214 @Composable 215 private fun BackHandler() { 216 val targetSceneForBackOrNull = 217 scene(state.transitionState.currentScene).userActions[Back]?.toScene 218 BackHandler(enabled = targetSceneForBackOrNull != null) { 219 targetSceneForBackOrNull?.let { targetSceneForBack -> 220 // TODO(b/290184746): Handle predictive back and use result.distance if specified. 221 if (state.canChangeScene(targetSceneForBack)) { 222 with(state) { coroutineScope.onChangeScene(targetSceneForBack) } 223 } 224 } 225 } 226 } 227 228 private fun scenesToCompose(): List<Scene> { 229 val transitions = state.currentTransitions 230 return if (transitions.isEmpty()) { 231 listOf(scene(state.transitionState.currentScene)) 232 } else { 233 buildList { 234 val visited = mutableSetOf<SceneKey>() 235 fun maybeAdd(sceneKey: SceneKey) { 236 if (visited.add(sceneKey)) { 237 add(scene(sceneKey)) 238 } 239 } 240 241 // Compose the new scene we are going to first. 242 transitions.fastForEachReversed { transition -> 243 maybeAdd(transition.toScene) 244 maybeAdd(transition.fromScene) 245 } 246 } 247 } 248 } 249 250 internal fun setScenesTargetSizeForTest(size: IntSize) { 251 scenes.values.forEach { it.targetSize = size } 252 } 253 } 254 255 private data class LayoutElement(private val layoutImpl: SceneTransitionLayoutImpl) : 256 ModifierNodeElement<LayoutNode>() { createnull257 override fun create(): LayoutNode = LayoutNode(layoutImpl) 258 259 override fun update(node: LayoutNode) { 260 node.layoutImpl = layoutImpl 261 } 262 } 263 264 private class LayoutNode(var layoutImpl: SceneTransitionLayoutImpl) : 265 Modifier.Node(), ApproachLayoutModifierNode { isMeasurementApproachInProgressnull266 override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean { 267 return layoutImpl.state.isTransitioning() 268 } 269 270 @ExperimentalComposeUiApi approachMeasurenull271 override fun ApproachMeasureScope.approachMeasure( 272 measurable: Measurable, 273 constraints: Constraints, 274 ): MeasureResult { 275 // Measure content normally. 276 val placeable = measurable.measure(constraints) 277 278 val width: Int 279 val height: Int 280 val transition = layoutImpl.state.currentTransition 281 if (transition == null) { 282 width = placeable.width 283 height = placeable.height 284 } else { 285 // Interpolate the size. 286 val fromSize = layoutImpl.scene(transition.fromScene).targetSize 287 val toSize = layoutImpl.scene(transition.toScene).targetSize 288 289 // Optimization: make sure we don't read state.progress if fromSize == 290 // toSize to avoid running this code every frame when the layout size does 291 // not change. 292 if (fromSize == toSize) { 293 width = fromSize.width 294 height = fromSize.height 295 } else { 296 val overscrollSpec = transition.currentOverscrollSpec 297 val progress = 298 when { 299 overscrollSpec == null -> transition.progress 300 overscrollSpec.scene == transition.toScene -> 1f 301 else -> 0f 302 } 303 304 val size = lerp(fromSize, toSize, progress) 305 width = size.width.coerceAtLeast(0) 306 height = size.height.coerceAtLeast(0) 307 } 308 } 309 310 return layout(width, height) { placeable.place(0, 0) } 311 } 312 } 313