• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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