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