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.compose.runtime.Composable
20 import androidx.compose.runtime.DisposableEffect
21 import androidx.compose.runtime.SideEffect
22 import androidx.compose.runtime.derivedStateOf
23 import androidx.compose.runtime.getValue
24 import androidx.compose.runtime.mutableStateOf
25 import androidx.compose.runtime.remember
26 import androidx.compose.runtime.setValue
27 import androidx.compose.runtime.snapshots.Snapshot
28 import androidx.compose.runtime.snapshots.SnapshotStateMap
29 import androidx.compose.ui.ExperimentalComposeUiApi
30 import androidx.compose.ui.Modifier
31 import androidx.compose.ui.draw.drawWithContent
32 import androidx.compose.ui.geometry.Offset
33 import androidx.compose.ui.geometry.isSpecified
34 import androidx.compose.ui.geometry.lerp
35 import androidx.compose.ui.graphics.graphicsLayer
36 import androidx.compose.ui.layout.IntermediateMeasureScope
37 import androidx.compose.ui.layout.Measurable
38 import androidx.compose.ui.layout.Placeable
39 import androidx.compose.ui.layout.intermediateLayout
40 import androidx.compose.ui.platform.testTag
41 import androidx.compose.ui.unit.Constraints
42 import androidx.compose.ui.unit.IntSize
43 import androidx.compose.ui.unit.round
44 import com.android.compose.animation.scene.transformation.PropertyTransformation
45 import com.android.compose.modifiers.thenIf
46 import com.android.compose.ui.util.lerp
47
48 /** An element on screen, that can be composed in one or more scenes. */
49 internal class Element(val key: ElementKey) {
50 /**
51 * The last offset assigned to this element, relative to the SceneTransitionLayout containing
52 * it.
53 */
54 var lastOffset = Offset.Unspecified
55
56 /** The last size assigned to this element. */
57 var lastSize = SizeUnspecified
58
59 /** The last alpha assigned to this element. */
60 var lastAlpha = 1f
61
62 /** The mapping between a scene and the values/state this element has in that scene, if any. */
63 val sceneValues = SnapshotStateMap<SceneKey, SceneValues>()
64
65 override fun toString(): String {
66 return "Element(key=$key)"
67 }
68
69 /** The target values of this element in a given scene. */
70 class SceneValues {
71 var size by mutableStateOf(SizeUnspecified)
72 var offset by mutableStateOf(Offset.Unspecified)
73 val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>()
74 }
75
76 /** A shared value of this element. */
77 class SharedValue<T>(val key: ValueKey, initialValue: T) {
78 var value by mutableStateOf(initialValue)
79 }
80
81 companion object {
82 val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
83 }
84 }
85
86 /** The implementation of [SceneScope.element]. */
87 @Composable
88 @OptIn(ExperimentalComposeUiApi::class)
elementnull89 internal fun Modifier.element(
90 layoutImpl: SceneTransitionLayoutImpl,
91 scene: Scene,
92 key: ElementKey,
93 ): Modifier {
94 val sceneValues = remember(scene, key) { Element.SceneValues() }
95 val element =
96 // Get the element associated to [key] if it was already composed in another scene,
97 // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
98 // withoutReadObservation() because there is no need to recompose when that map is mutated.
99 Snapshot.withoutReadObservation {
100 val element =
101 layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
102 val previousValues = element.sceneValues[scene.key]
103 if (previousValues == null) {
104 element.sceneValues[scene.key] = sceneValues
105 } else if (previousValues != sceneValues) {
106 error("$key was composed multiple times in $scene")
107 }
108
109 element
110 }
111
112 DisposableEffect(scene, sceneValues, element) {
113 onDispose {
114 element.sceneValues.remove(scene.key)
115
116 // This was the last scene this element was in, so remove it from the map.
117 if (element.sceneValues.isEmpty()) {
118 layoutImpl.elements.remove(element.key)
119 }
120 }
121 }
122
123 val alpha =
124 remember(layoutImpl, element, scene) {
125 derivedStateOf { elementAlpha(layoutImpl, element, scene) }
126 }
127 val isOpaque by remember(alpha) { derivedStateOf { alpha.value == 1f } }
128 SideEffect {
129 if (isOpaque && element.lastAlpha != 1f) {
130 element.lastAlpha = 1f
131 }
132 }
133
134 return drawWithContent {
135 if (shouldDrawElement(layoutImpl, scene, element)) {
136 drawContent()
137 }
138 }
139 .modifierTransformations(layoutImpl, scene, element, sceneValues)
140 .intermediateLayout { measurable, constraints ->
141 val placeable =
142 measure(layoutImpl, scene, element, sceneValues, measurable, constraints)
143 layout(placeable.width, placeable.height) {
144 place(layoutImpl, scene, element, sceneValues, placeable, placementScope = this)
145 }
146 }
147 .thenIf(!isOpaque) {
148 Modifier.graphicsLayer {
149 val alpha = alpha.value
150 this.alpha = alpha
151 element.lastAlpha = alpha
152 }
153 }
154 .testTag(key.name)
155 }
156
shouldDrawElementnull157 private fun shouldDrawElement(
158 layoutImpl: SceneTransitionLayoutImpl,
159 scene: Scene,
160 element: Element,
161 ): Boolean {
162 val state = layoutImpl.state.transitionState
163
164 // Always draw the element if there is no ongoing transition or if the element is not shared.
165 if (
166 state !is TransitionState.Transition ||
167 state.fromScene == state.toScene ||
168 !layoutImpl.isTransitionReady(state) ||
169 state.fromScene !in element.sceneValues ||
170 state.toScene !in element.sceneValues
171 ) {
172 return true
173 }
174
175 val otherScene =
176 layoutImpl.scenes.getValue(
177 if (scene.key == state.fromScene) {
178 state.toScene
179 } else {
180 state.fromScene
181 }
182 )
183
184 // When the element is shared, draw the one in the highest scene unless it is a background, i.e.
185 // it is usually drawn below everything else.
186 val isHighestScene = scene.zIndex > otherScene.zIndex
187 return if (element.key.isBackground) {
188 !isHighestScene
189 } else {
190 isHighestScene
191 }
192 }
193
194 /**
195 * Chain the [com.android.compose.animation.scene.transformation.ModifierTransformation] applied
196 * throughout the current transition, if any.
197 */
Modifiernull198 private fun Modifier.modifierTransformations(
199 layoutImpl: SceneTransitionLayoutImpl,
200 scene: Scene,
201 element: Element,
202 sceneValues: Element.SceneValues,
203 ): Modifier {
204 when (val state = layoutImpl.state.transitionState) {
205 is TransitionState.Idle -> return this
206 is TransitionState.Transition -> {
207 val fromScene = state.fromScene
208 val toScene = state.toScene
209 if (fromScene == toScene) {
210 // Same as idle.
211 return this
212 }
213
214 return layoutImpl.transitions
215 .transitionSpec(fromScene, state.toScene)
216 .transformations(element.key)
217 .modifier
218 .fold(this) { modifier, transformation ->
219 with(transformation) {
220 modifier.transform(layoutImpl, scene, element, sceneValues)
221 }
222 }
223 }
224 }
225 }
226
elementAlphanull227 private fun elementAlpha(
228 layoutImpl: SceneTransitionLayoutImpl,
229 element: Element,
230 scene: Scene
231 ): Float {
232 return computeValue(
233 layoutImpl,
234 scene,
235 element,
236 sceneValue = { 1f },
237 transformation = { it.alpha },
238 idleValue = 1f,
239 currentValue = { 1f },
240 lastValue = { element.lastAlpha },
241 ::lerp,
242 )
243 .coerceIn(0f, 1f)
244 }
245
246 @OptIn(ExperimentalComposeUiApi::class)
measurenull247 private fun IntermediateMeasureScope.measure(
248 layoutImpl: SceneTransitionLayoutImpl,
249 scene: Scene,
250 element: Element,
251 sceneValues: Element.SceneValues,
252 measurable: Measurable,
253 constraints: Constraints,
254 ): Placeable {
255 // Update the size this element has in this scene when idle.
256 val targetSizeInScene = lookaheadSize
257 if (targetSizeInScene != sceneValues.size) {
258 // TODO(b/290930950): Better handle when this changes to avoid instant size jumps.
259 sceneValues.size = targetSizeInScene
260 }
261
262 // Some lambdas called (max once) by computeValue() will need to measure [measurable], in which
263 // case we store the resulting placeable here to make sure the element is not measured more than
264 // once.
265 var maybePlaceable: Placeable? = null
266
267 fun Placeable.size() = IntSize(width, height)
268
269 val targetSize =
270 computeValue(
271 layoutImpl,
272 scene,
273 element,
274 sceneValue = { it.size },
275 transformation = { it.size },
276 idleValue = lookaheadSize,
277 currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
278 lastValue = {
279 val lastSize = element.lastSize
280 if (lastSize != Element.SizeUnspecified) {
281 lastSize
282 } else {
283 measurable.measure(constraints).also { maybePlaceable = it }.size()
284 }
285 },
286 ::lerp,
287 )
288
289 val placeable =
290 maybePlaceable
291 ?: measurable.measure(
292 Constraints.fixed(
293 targetSize.width.coerceAtLeast(0),
294 targetSize.height.coerceAtLeast(0),
295 )
296 )
297
298 element.lastSize = placeable.size()
299 return placeable
300 }
301
302 @OptIn(ExperimentalComposeUiApi::class)
IntermediateMeasureScopenull303 private fun IntermediateMeasureScope.place(
304 layoutImpl: SceneTransitionLayoutImpl,
305 scene: Scene,
306 element: Element,
307 sceneValues: Element.SceneValues,
308 placeable: Placeable,
309 placementScope: Placeable.PlacementScope,
310 ) {
311 with(placementScope) {
312 // Update the offset (relative to the SceneTransitionLayout) this element has in this scene
313 // when idle.
314 val coords = coordinates!!
315 val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
316 if (targetOffsetInScene != sceneValues.offset) {
317 // TODO(b/290930950): Better handle when this changes to avoid instant offset jumps.
318 sceneValues.offset = targetOffsetInScene
319 }
320
321 val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
322 val targetOffset =
323 computeValue(
324 layoutImpl,
325 scene,
326 element,
327 sceneValue = { it.offset },
328 transformation = { it.offset },
329 idleValue = targetOffsetInScene,
330 currentValue = { currentOffset },
331 lastValue = {
332 val lastValue = element.lastOffset
333 if (lastValue.isSpecified) {
334 lastValue
335 } else {
336 currentOffset
337 }
338 },
339 ::lerp,
340 )
341
342 element.lastOffset = targetOffset
343 placeable.place((targetOffset - currentOffset).round())
344 }
345 }
346
347 /**
348 * Return the value that should be used depending on the current layout state and transition.
349 *
350 * Important: This function must remain inline because of all the lambda parameters. These lambdas
351 * are necessary because getting some of them might require some computation, like measuring a
352 * Measurable.
353 *
354 * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
355 * @param scene the scene containing [element].
356 * @param element the element being animated.
357 * @param sceneValue the value being animated.
358 * @param transformation the transformation associated to the value being animated.
359 * @param idleValue the value when idle, i.e. when there is no transition happening.
360 * @param currentValue the value that would be used if it is not transformed. Note that this is
361 * different than [idleValue] even if the value is not transformed directly because it could be
362 * impacted by the transformations on other elements, like a parent that is being translated or
363 * resized.
364 * @param lastValue the last value that was used. This should be equal to [currentValue] if this is
365 * the first time the value is set.
366 * @param lerp the linear interpolation function used to interpolate between two values of this
367 * value type.
368 */
computeValuenull369 private inline fun <T> computeValue(
370 layoutImpl: SceneTransitionLayoutImpl,
371 scene: Scene,
372 element: Element,
373 sceneValue: (Element.SceneValues) -> T,
374 transformation: (ElementTransformations) -> PropertyTransformation<T>?,
375 idleValue: T,
376 currentValue: () -> T,
377 lastValue: () -> T,
378 lerp: (T, T, Float) -> T,
379 ): T {
380 val state = layoutImpl.state.transitionState
381
382 // There is no ongoing transition.
383 if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
384 return idleValue
385 }
386
387 // A transition was started but it's not ready yet (not all elements have been composed/laid
388 // out yet). Use the last value that was set, to make sure elements don't unexpectedly jump.
389 if (!layoutImpl.isTransitionReady(state)) {
390 return lastValue()
391 }
392
393 val fromScene = state.fromScene
394 val toScene = state.toScene
395 val fromValues = element.sceneValues[fromScene]
396 val toValues = element.sceneValues[toScene]
397
398 if (fromValues == null && toValues == null) {
399 error("This should not happen, element $element is neither in $fromScene or $toScene")
400 }
401
402 // TODO(b/291053278): Handle overscroll correctly. We should probably coerce between [0f, 1f]
403 // here and consume overflows at drawing time, somehow reusing Compose OverflowEffect or some
404 // similar mechanism.
405 val transitionProgress = state.progress
406
407 // The element is shared: interpolate between the value in fromScene and the value in toScene.
408 // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
409 // elements follow the finger direction.
410 if (fromValues != null && toValues != null) {
411 return lerp(
412 sceneValue(fromValues),
413 sceneValue(toValues),
414 transitionProgress,
415 )
416 }
417
418 val transformation =
419 transformation(
420 layoutImpl.transitions.transitionSpec(fromScene, toScene).transformations(element.key)
421 )
422 // If there is no transformation explicitly associated to this element value, let's use
423 // the value given by the system (like the current position and size given by the layout
424 // pass).
425 ?: return currentValue()
426
427 // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
428 // end (for leaving elements) of the transition.
429 val targetValue =
430 transformation.transform(
431 layoutImpl,
432 scene,
433 element,
434 fromValues ?: toValues!!,
435 state,
436 idleValue,
437 )
438
439 // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range.
440 val rangeProgress = transformation.range?.progress(transitionProgress) ?: transitionProgress
441
442 // Interpolate between the value at rest and the value before entering/after leaving.
443 val isEntering = fromValues == null
444 return if (isEntering) {
445 lerp(targetValue, idleValue, rangeProgress)
446 } else {
447 lerp(idleValue, targetValue, rangeProgress)
448 }
449 }
450