1 /*
<lambda>null2  * Copyright (C) 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.compose.foundation.layout.Box
20 import androidx.compose.foundation.layout.BoxScope
21 import androidx.compose.runtime.Composable
22 import androidx.compose.runtime.derivedStateOf
23 import androidx.compose.runtime.getValue
24 import androidx.compose.runtime.movableContentOf
25 import androidx.compose.runtime.remember
26 import androidx.compose.ui.Modifier
27 import androidx.compose.ui.layout.Layout
28 import androidx.compose.ui.unit.IntSize
29 import com.android.compose.animation.scene.content.Content
30 import com.android.compose.animation.scene.content.state.TransitionState
31 
32 @Composable
33 internal fun Element(
34     layoutImpl: SceneTransitionLayoutImpl,
35     sceneOrOverlay: Content,
36     key: ElementKey,
37     modifier: Modifier,
38     content: @Composable ElementScope<ElementContentScope>.() -> Unit,
39 ) {
40     Box(modifier.element(layoutImpl, sceneOrOverlay, key), propagateMinConstraints = true) {
41         val contentScope = sceneOrOverlay.scope
42         val boxScope = this
43         val elementScope =
44             remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) {
45                 ElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope)
46             }
47 
48         content(elementScope)
49     }
50 }
51 
52 @Composable
MovableElementnull53 internal fun MovableElement(
54     layoutImpl: SceneTransitionLayoutImpl,
55     sceneOrOverlay: Content,
56     key: MovableElementKey,
57     modifier: Modifier,
58     content: @Composable ElementScope<MovableElementContentScope>.() -> Unit,
59 ) {
60     check(key.contentPicker.contents.contains(sceneOrOverlay.key)) {
61         val elementName = key.debugName
62         val contentName = sceneOrOverlay.key.debugName
63         "MovableElement $elementName was composed in content $contentName but the " +
64             "MovableElementKey($elementName).contentPicker.contents does not contain $contentName"
65     }
66 
67     Box(modifier.element(layoutImpl, sceneOrOverlay, key), propagateMinConstraints = true) {
68         val contentScope = sceneOrOverlay.scope
69         val boxScope = this
70         val elementScope =
71             remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) {
72                 MovableElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope)
73             }
74 
75         content(elementScope)
76     }
77 }
78 
79 private abstract class BaseElementScope<ContentScope>(
80     private val layoutImpl: SceneTransitionLayoutImpl,
81     private val element: ElementKey,
82     private val sceneOrOverlay: Content,
83 ) : ElementScope<ContentScope> {
84     @Composable
animateElementValueAsStatenull85     override fun <T> animateElementValueAsState(
86         value: T,
87         key: ValueKey,
88         type: SharedValueType<T, *>,
89         canOverflow: Boolean,
90     ): AnimatedState<T> {
91         return animateSharedValueAsState(
92             layoutImpl,
93             sceneOrOverlay.key,
94             element,
95             key,
96             value,
97             type,
98             canOverflow,
99         )
100     }
101 }
102 
103 private class ElementScopeImpl(
104     layoutImpl: SceneTransitionLayoutImpl,
105     element: ElementKey,
106     content: Content,
107     private val delegateContentScope: ContentScope,
108     private val boxScope: BoxScope,
109 ) : BaseElementScope<ElementContentScope>(layoutImpl, element, content) {
110     private val contentScope =
111         object : ElementContentScope, ContentScope by delegateContentScope, BoxScope by boxScope {}
112 
113     @Composable
contentnull114     override fun content(content: @Composable ElementContentScope.() -> Unit) {
115         contentScope.content()
116     }
117 }
118 
119 private class MovableElementScopeImpl(
120     private val layoutImpl: SceneTransitionLayoutImpl,
121     private val element: MovableElementKey,
122     private val content: Content,
123     private val baseContentScope: BaseContentScope,
124     private val boxScope: BoxScope,
125 ) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, content) {
126     private val contentScope =
127         object :
128             MovableElementContentScope,
129             BaseContentScope by baseContentScope,
<lambda>null130             BoxScope by boxScope {}
131 
132     @Composable
contentnull133     override fun content(content: @Composable MovableElementContentScope.() -> Unit) {
134         // Whether we should compose the movable element here. The scene picker logic to know in
135         // which scene we should compose/draw a movable element might depend on the current
136         // transition progress, so we put this in a derivedStateOf to prevent many recompositions
137         // during the transition.
138         // TODO(b/317026105): Use derivedStateOf only if the scene picker reads the progress in its
139         // logic.
140         val contentKey = this@MovableElementScopeImpl.content.key
141         val shouldComposeMovableElement by
142             remember(layoutImpl, contentKey, element) {
143                 derivedStateOf { shouldComposeMovableElement(layoutImpl, contentKey, element) }
144             }
145 
146         if (shouldComposeMovableElement) {
147             val movableContent: MovableElementContent =
148                 layoutImpl.movableContents[element]
149                     ?: movableContentOf { content: @Composable () -> Unit -> content() }
150                         .also { layoutImpl.movableContents[element] = it }
151 
152             // Important: Don't introduce any parent Box or other layout here, because contentScope
153             // delegates its BoxScope implementation to the Box where this content() function is
154             // called, so it's important that this movableContent is composed directly under that
155             // Box.
156             movableContent { contentScope.content() }
157         } else {
158             // If we are not composed, we still need to lay out an empty space with the same *target
159             // size* as its movable content, i.e. the same *size when idle*. During transitions,
160             // this size will be used to interpolate the transition size, during the intermediate
161             // layout pass.
162             //
163             // Important: Like in Modifier.element(), we read the transition states during
164             // composition then pass them to Layout to make sure that composition sees new states
165             // before layout and drawing.
166             val transitionStates = layoutImpl.state.transitionStates
167             Layout { _, _ ->
168                 // No need to measure or place anything.
169                 val size =
170                     placeholderContentSize(
171                         layoutImpl = layoutImpl,
172                         content = contentKey,
173                         element = layoutImpl.elements.getValue(element),
174                         elementKey = element,
175                         transitionStates = transitionStates,
176                     )
177                 layout(size.width, size.height) {}
178             }
179         }
180     }
181 }
182 
shouldComposeMovableElementnull183 private fun shouldComposeMovableElement(
184     layoutImpl: SceneTransitionLayoutImpl,
185     content: ContentKey,
186     element: MovableElementKey,
187 ): Boolean {
188     return when (
189         val elementState = movableElementState(element, layoutImpl.state.transitionStates)
190     ) {
191         null ->
192             movableElementContentWhenIdle(layoutImpl, element, layoutImpl.state.transitionState) ==
193                 content
194         is TransitionState.Idle ->
195             movableElementContentWhenIdle(layoutImpl, element, elementState) == content
196         is TransitionState.Transition -> {
197             // During transitions, always compose movable elements in the scene picked by their
198             // content picker.
199             shouldComposeMoveableElement(
200                 layoutImpl,
201                 content,
202                 element,
203                 elementState,
204                 element.contentPicker.contents,
205             )
206         }
207     }
208 }
209 
shouldComposeMoveableElementnull210 private fun shouldComposeMoveableElement(
211     layoutImpl: SceneTransitionLayoutImpl,
212     content: ContentKey,
213     elementKey: ElementKey,
214     transition: TransitionState.Transition,
215     containingContents: Set<ContentKey>,
216 ): Boolean {
217     val overscrollContent = transition.currentOverscrollSpec?.content
218     if (overscrollContent != null) {
219         return when (transition) {
220             // If we are overscrolling between scenes, only place/compose the element in the
221             // overscrolling scene.
222             is TransitionState.Transition.ChangeScene -> content == overscrollContent
223 
224             // If we are overscrolling an overlay, place/compose the element if [content] is the
225             // overscrolling content or if [content] is the current scene and the overscrolling
226             // overlay does not contain the element.
227             is TransitionState.Transition.ReplaceOverlay,
228             is TransitionState.Transition.ShowOrHideOverlay ->
229                 content == overscrollContent ||
230                     (content == transition.currentScene &&
231                         !containingContents.contains(overscrollContent))
232         }
233     }
234 
235     val scenePicker = elementKey.contentPicker
236     val pickedScene =
237         scenePicker.contentDuringTransition(
238             element = elementKey,
239             transition = transition,
240             fromContentZIndex = layoutImpl.content(transition.fromContent).zIndex,
241             toContentZIndex = layoutImpl.content(transition.toContent).zIndex,
242         )
243 
244     return pickedScene == content
245 }
246 
movableElementStatenull247 private fun movableElementState(
248     element: MovableElementKey,
249     transitionStates: List<TransitionState>,
250 ): TransitionState? {
251     val contents = element.contentPicker.contents
252     return elementState(transitionStates, isInContent = { contents.contains(it) })
253 }
254 
movableElementContentWhenIdlenull255 private fun movableElementContentWhenIdle(
256     layoutImpl: SceneTransitionLayoutImpl,
257     element: MovableElementKey,
258     elementState: TransitionState,
259 ): ContentKey {
260     val contents = element.contentPicker.contents
261     return elementContentWhenIdle(layoutImpl, elementState, isInContent = { contents.contains(it) })
262 }
263 
264 /**
265  * Return the size of the placeholder/space that is composed when the movable content is not
266  * composed in a scene.
267  */
placeholderContentSizenull268 private fun placeholderContentSize(
269     layoutImpl: SceneTransitionLayoutImpl,
270     content: ContentKey,
271     element: Element,
272     elementKey: MovableElementKey,
273     transitionStates: List<TransitionState>,
274 ): IntSize {
275     // If the content of the movable element was already composed in this scene before, use that
276     // target size.
277     val targetValueInScene = element.stateByContent.getValue(content).targetSize
278     if (targetValueInScene != Element.SizeUnspecified) {
279         return targetValueInScene
280     }
281 
282     fun TransitionState.Transition.otherContent(): ContentKey {
283         return if (fromContent == content) toContent else fromContent
284     }
285 
286     // If the element content was already composed in the other overlay/scene, we use that
287     // target size assuming it doesn't change between scenes.
288     // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is
289     // not true.
290     val otherContent =
291         when (val state = movableElementState(elementKey, transitionStates)) {
292             null -> return IntSize.Zero
293             is TransitionState.Idle -> movableElementContentWhenIdle(layoutImpl, elementKey, state)
294             is TransitionState.Transition.ReplaceOverlay -> {
295                 state.otherContent().takeIf { it in element.stateByContent } ?: state.currentScene
296             }
297             is TransitionState.Transition -> state.otherContent()
298         }
299 
300     val targetValueInOtherContent = element.stateByContent[otherContent]?.targetSize
301     if (targetValueInOtherContent != null && targetValueInOtherContent != Element.SizeUnspecified) {
302         return targetValueInOtherContent
303     }
304 
305     return IntSize.Zero
306 }
307