1 /*
2 * Copyright (C) 2024 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.content
18
19 import android.annotation.SuppressLint
20 import androidx.compose.foundation.LocalOverscrollFactory
21 import androidx.compose.foundation.OverscrollEffect
22 import androidx.compose.foundation.OverscrollFactory
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.BoxScope
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.CompositionLocalProvider
27 import androidx.compose.runtime.Stable
28 import androidx.compose.runtime.getValue
29 import androidx.compose.runtime.mutableFloatStateOf
30 import androidx.compose.runtime.mutableLongStateOf
31 import androidx.compose.runtime.mutableStateOf
32 import androidx.compose.runtime.remember
33 import androidx.compose.runtime.setValue
34 import androidx.compose.ui.Modifier
35 import androidx.compose.ui.layout.ApproachLayoutModifierNode
36 import androidx.compose.ui.layout.ApproachMeasureScope
37 import androidx.compose.ui.layout.LookaheadScope
38 import androidx.compose.ui.layout.Measurable
39 import androidx.compose.ui.layout.MeasureResult
40 import androidx.compose.ui.layout.MeasureScope
41 import androidx.compose.ui.node.DelegatingNode
42 import androidx.compose.ui.node.ModifierNodeElement
43 import androidx.compose.ui.platform.testTag
44 import androidx.compose.ui.unit.Constraints
45 import androidx.compose.ui.unit.IntSize
46 import com.android.compose.animation.scene.Ancestor
47 import com.android.compose.animation.scene.AnimatedState
48 import com.android.compose.animation.scene.ContentKey
49 import com.android.compose.animation.scene.ContentScope
50 import com.android.compose.animation.scene.Element
51 import com.android.compose.animation.scene.ElementContentScope
52 import com.android.compose.animation.scene.ElementKey
53 import com.android.compose.animation.scene.ElementScope
54 import com.android.compose.animation.scene.ElementStateScope
55 import com.android.compose.animation.scene.ElementWithValues
56 import com.android.compose.animation.scene.InternalContentScope
57 import com.android.compose.animation.scene.MovableElement
58 import com.android.compose.animation.scene.MovableElementContentScope
59 import com.android.compose.animation.scene.MovableElementKey
60 import com.android.compose.animation.scene.SceneTransitionLayoutForTesting
61 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
62 import com.android.compose.animation.scene.SceneTransitionLayoutScope
63 import com.android.compose.animation.scene.SceneTransitionLayoutState
64 import com.android.compose.animation.scene.SharedValueType
65 import com.android.compose.animation.scene.UserAction
66 import com.android.compose.animation.scene.UserActionResult
67 import com.android.compose.animation.scene.ValueKey
68 import com.android.compose.animation.scene.animateSharedValueAsState
69 import com.android.compose.animation.scene.effect.GestureEffect
70 import com.android.compose.animation.scene.element
71 import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions
72 import com.android.compose.gesture.NestedScrollControlState
73 import com.android.compose.gesture.NestedScrollableBound
74 import com.android.compose.gesture.nestedScrollController
75 import com.android.compose.modifiers.thenIf
76 import com.android.compose.ui.graphics.ContainerNode
77 import com.android.compose.ui.graphics.ContainerState
78 import kotlin.math.pow
79
80 /** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */
81 @Stable
82 internal sealed class Content(
83 open val key: ContentKey,
84 val layoutImpl: SceneTransitionLayoutImpl,
85 content: @Composable InternalContentScope.() -> Unit,
86 actions: Map<UserAction.Resolved, UserActionResult>,
87 zIndex: Float,
88 globalZIndex: Long,
89 effectFactory: OverscrollFactory,
90 ) {
91 private val nestedScrollControlState = NestedScrollControlState()
92 internal val scope = ContentScopeImpl(layoutImpl, content = this, nestedScrollControlState)
93 val containerState = ContainerState()
94
95 // Important: All fields in this class should be backed by State given that contents are updated
96 // directly during composition, outside of a SideEffect, or are observed during composition,
97 // layout or drawing.
98 var content by mutableStateOf(content)
99 var targetSize by mutableStateOf(Element.SizeUnspecified)
100 var userActions by mutableStateOf(actions)
101 var zIndex by mutableFloatStateOf(zIndex)
102
103 /**
104 * The globalZIndex is a zIndex that indicates the z order of each content across any nested
105 * STLs. This is done by dividing the number range of a Long into chunks of three digits. As
106 * Long.MAX_VALUE is a bit larger than 1e18 we start the first level at 1e15 to give at least
107 * 1000 contents space. The first level of nesting depth will occupy the 3 highest digits and
108 * with each level we continue into the next three. Therefore the parent z order will have
109 * priority and their children have room to order themselves within the "less significant bits".
110 *
111 * As an example, imagine the following tree of nested scenes:
112 * ```
113 * / \
114 * A01 A02 -- nestingDepth 0
115 * / \ |
116 * B01 B02 C01 -- nestingDepth 1
117 * |
118 * D01 -- nestingDepth 2
119 * ```
120 *
121 * The zIndex values would be:
122 * ```
123 * A01: 1e15 (1_000_000_000_000_000)
124 * A02: 2e15 (2_000_000_000_000_000)
125 * B01: 1.001e15 (1_001_000_000_000_000)
126 * B02: 1.002e15 (1_002_000_000_000_000)
127 * C01: 2.001e15 (2_001_000_000_000_000)
128 * D01: 1.002001e15 (1_002_001_000_000_000)
129 * ```
130 *
131 * Therefore the order of zIndexes will correctly be: A01, B01, B02, D01, A02, C01, which
132 * corresponds to a Pre-order traversal of the tree.
133 *
134 * Since composition of the tree does not happen all at once we can't do a Pre-order traversal
135 * right away without allocating resources to build and manage the tree structure through all
136 * updates. Using this method we have stable zIndexes at time of composition of each content
137 * independently with the only drawback that contents per each STL are limited to 999 and
138 * nesting depth is limited to 6 (18 / 3).
139 */
140 var globalZIndex by mutableLongStateOf(globalZIndex)
141
142 companion object {
calculateGlobalZIndexnull143 fun calculateGlobalZIndex(
144 parentGlobalZIndex: Long,
145 localZIndex: Int,
146 nestingDepth: Int,
147 ): Long {
148 require(nestingDepth in 0..5) { "NestingDepth of STLs can be at most 5." }
149 require(localZIndex in 1..999) { "A scene can have at most 999 contents." }
150 val offsetForDepth = 10.0.pow((5 - nestingDepth) * 3).toLong()
151 return parentGlobalZIndex + offsetForDepth * localZIndex
152 }
153 }
154
155 private var lastFactory by mutableStateOf(effectFactory)
156 var verticalEffects by mutableStateOf(ContentEffects(effectFactory))
157 private set
158
159 var horizontalEffects by mutableStateOf(ContentEffects(effectFactory))
160 private set
161
162 @SuppressLint("NotConstructor")
163 @Composable
Contentnull164 fun Content(modifier: Modifier = Modifier, isInvisible: Boolean = false) {
165 // If this content has a custom factory, provide it to the content so that the factory is
166 // automatically used when calling rememberOverscrollEffect().
167 val isElevationPossible =
168 layoutImpl.state.isElevationPossible(content = key, element = null)
169 Box(
170 modifier.then(ContentElement(this, isElevationPossible, isInvisible)).thenIf(
171 layoutImpl.implicitTestTags
172 ) {
173 Modifier.testTag(key.testTag)
174 }
175 ) {
176 CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) {
177 scope.content()
178 }
179 }
180 }
181
areNestedSwipesAllowednull182 fun areNestedSwipesAllowed(): Boolean = nestedScrollControlState.isOuterScrollAllowed
183
184 fun maybeUpdateEffects(effectFactory: OverscrollFactory) {
185 if (effectFactory != lastFactory) {
186 lastFactory = effectFactory
187 verticalEffects = ContentEffects(effectFactory)
188 horizontalEffects = ContentEffects(effectFactory)
189 }
190 }
191 }
192
193 private data class ContentElement(
194 private val content: Content,
195 private val isElevationPossible: Boolean,
196 private val isInvisible: Boolean,
197 ) : ModifierNodeElement<ContentNode>() {
createnull198 override fun create(): ContentNode = ContentNode(content, isElevationPossible, isInvisible)
199
200 override fun update(node: ContentNode) {
201 node.update(content, isElevationPossible, isInvisible)
202 }
203 }
204
205 private class ContentNode(
206 private var content: Content,
207 private var isElevationPossible: Boolean,
208 private var isInvisible: Boolean,
209 ) : DelegatingNode(), ApproachLayoutModifierNode {
210 private var containerDelegate = containerDelegate(isElevationPossible)
211
containerDelegatenull212 private fun containerDelegate(isElevationPossible: Boolean): ContainerNode? {
213 return if (isElevationPossible) delegate(ContainerNode(content.containerState)) else null
214 }
215
onDetachnull216 override fun onDetach() {
217 this.content.targetSize = Element.SizeUnspecified
218 }
219
updatenull220 fun update(content: Content, isElevationPossible: Boolean, isInvisible: Boolean) {
221 if (content != this.content) {
222 this.content.targetSize = Element.SizeUnspecified
223 this.content = content
224 }
225
226 if (content != this.content || isElevationPossible != this.isElevationPossible) {
227 this.isElevationPossible = isElevationPossible
228
229 containerDelegate?.let { undelegate(it) }
230 containerDelegate = containerDelegate(isElevationPossible)
231 }
232
233 this.isInvisible = isInvisible
234 }
235
isMeasurementApproachInProgressnull236 override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean = false
237
238 override fun MeasureScope.measure(
239 measurable: Measurable,
240 constraints: Constraints,
241 ): MeasureResult {
242 check(isLookingAhead)
243 return measurable.measure(constraints).run {
244 content.targetSize = IntSize(width, height)
245 layout(width, height) {
246 if (!isInvisible) {
247 place(0, 0, zIndex = content.zIndex)
248 }
249 }
250 }
251 }
252
approachMeasurenull253 override fun ApproachMeasureScope.approachMeasure(
254 measurable: Measurable,
255 constraints: Constraints,
256 ): MeasureResult {
257 return measurable.measure(constraints).run {
258 layout(width, height) {
259 if (!isInvisible) {
260 place(0, 0, zIndex = content.zIndex)
261 }
262 }
263 }
264 }
265 }
266
267 internal class ContentEffects(factory: OverscrollFactory) {
268 val overscrollEffect = factory.createOverscrollEffect()
269 val gestureEffect = GestureEffect(overscrollEffect)
270 }
271
272 internal class ContentScopeImpl(
273 private val layoutImpl: SceneTransitionLayoutImpl,
274 private val content: Content,
275 private val nestedScrollControlState: NestedScrollControlState,
<lambda>null276 ) : InternalContentScope, ElementStateScope by layoutImpl.elementStateScope {
277 override val contentKey: ContentKey
278 get() = content.key
279
280 override val layoutState: SceneTransitionLayoutState = layoutImpl.state
281
282 override val lookaheadScope: LookaheadScope
283 get() = layoutImpl.lookaheadScope
284
285 override val verticalOverscrollEffect: OverscrollEffect
286 get() = content.verticalEffects.overscrollEffect
287
288 override val horizontalOverscrollEffect: OverscrollEffect
289 get() = content.horizontalEffects.overscrollEffect
290
291 override fun Modifier.element(key: ElementKey): Modifier {
292 return element(layoutImpl, content, key)
293 }
294
295 @Composable
296 override fun Element(
297 key: ElementKey,
298 modifier: Modifier,
299 content: @Composable BoxScope.() -> Unit,
300 ) {
301 Element(layoutImpl, this@ContentScopeImpl.content, key, modifier, content)
302 }
303
304 @Composable
305 override fun ElementWithValues(
306 key: ElementKey,
307 modifier: Modifier,
308 content: @Composable (ElementScope<ElementContentScope>.() -> Unit),
309 ) {
310 ElementWithValues(layoutImpl, this@ContentScopeImpl.content, key, modifier, content)
311 }
312
313 @Composable
314 override fun MovableElement(
315 key: MovableElementKey,
316 modifier: Modifier,
317 content: @Composable (ElementScope<MovableElementContentScope>.() -> Unit),
318 ) {
319 MovableElement(layoutImpl, this@ContentScopeImpl.content, key, modifier, content)
320 }
321
322 @Composable
323 override fun <T> animateContentValueAsState(
324 value: T,
325 key: ValueKey,
326 type: SharedValueType<T, *>,
327 canOverflow: Boolean,
328 ): AnimatedState<T> {
329 return animateSharedValueAsState(
330 layoutImpl = layoutImpl,
331 content = content.key,
332 element = null,
333 key = key,
334 value = value,
335 type = type,
336 canOverflow = canOverflow,
337 )
338 }
339
340 override fun Modifier.noResizeDuringTransitions(): Modifier {
341 return noResizeDuringTransitions(layoutState = layoutImpl.state)
342 }
343
344 override fun Modifier.disableSwipesWhenScrolling(bounds: NestedScrollableBound): Modifier {
345 return nestedScrollController(nestedScrollControlState, bounds)
346 }
347
348 @Composable
349 override fun NestedSceneTransitionLayout(
350 state: SceneTransitionLayoutState,
351 modifier: Modifier,
352 builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit,
353 ) {
354 NestedSceneTransitionLayoutForTesting(state, modifier, null, builder)
355 }
356
357 @Composable
358 override fun NestedSceneTransitionLayoutForTesting(
359 state: SceneTransitionLayoutState,
360 modifier: Modifier,
361 onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)?,
362 builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
363 ) {
364 val ancestors =
365 remember(layoutImpl, contentKey, layoutImpl.ancestors) {
366 layoutImpl.ancestors + Ancestor(layoutImpl, contentKey)
367 }
368 SceneTransitionLayoutForTesting(
369 state,
370 modifier,
371 onLayoutImpl = onLayoutImpl,
372 builder = builder,
373 sharedElementMap = layoutImpl.elements,
374 ancestors = ancestors,
375 lookaheadScope = layoutImpl.lookaheadScope,
376 implicitTestTags = layoutImpl.implicitTestTags,
377 )
378 }
379 }
380