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