1 /*
2 * Copyright (C) 2025 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.mechanics.demo.staging.behavior.reveal
18
19 import androidx.compose.runtime.getValue
20 import androidx.compose.runtime.mutableFloatStateOf
21 import androidx.compose.runtime.mutableStateOf
22 import androidx.compose.runtime.setValue
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.layout.ApproachLayoutModifierNode
25 import androidx.compose.ui.layout.ApproachMeasureScope
26 import androidx.compose.ui.layout.Measurable
27 import androidx.compose.ui.layout.MeasureResult
28 import androidx.compose.ui.node.ModifierNodeElement
29 import androidx.compose.ui.node.ObserverModifierNode
30 import androidx.compose.ui.node.TraversableNode
31 import androidx.compose.ui.node.observeReads
32 import androidx.compose.ui.unit.Constraints
33 import androidx.compose.ui.unit.IntSize
34 import com.android.compose.animation.scene.ContentScope
35 import com.android.mechanics.GestureContext
36 import com.android.mechanics.ProvidedGestureContext
37 import com.android.mechanics.spec.InputDirection
38
revealContainernull39 fun Modifier.revealContainer(contentScope: ContentScope): Modifier =
40 this.then(RevealContainerElement(contentScope))
41
42 internal class RevealContainerNode(var contentScope: ContentScope) :
43 Modifier.Node(),
44 TraversableNode,
45 ApproachLayoutModifierNode,
46 ObserverModifierNode,
47 GestureContext {
48
49 var containerHeight by mutableFloatStateOf(0f)
50 private set
51
52 override val traverseKey = TRAVERSAL_NODE_KEY
53
54 override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
55 return contentScope.layoutState.currentTransition != null
56 }
57
58 private var lastGestureContext by
59 mutableStateOf<GestureContext>(ProvidedGestureContext(dragOffset = 0f, InputDirection.Max))
60
61 override fun onAttach() {
62 updateGestureContext()
63 }
64
65 override fun ApproachMeasureScope.approachMeasure(
66 measurable: Measurable,
67 constraints: Constraints,
68 ): MeasureResult {
69 val animatedContainerHeight = constraints.maxHeight
70
71 containerHeight = animatedContainerHeight.toFloat()
72
73 val placeable = measurable.measure(constraints.copy(minHeight = animatedContainerHeight))
74 return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
75 }
76
77 companion object {
78 const val TRAVERSAL_NODE_KEY = "com.android.mechanics.demo.util.REVEAL_CONTAINER_NODE_KEY"
79 }
80
81 override fun onObservedReadsChanged() {
82 updateGestureContext()
83 }
84
85 private fun updateGestureContext() {
86 observeReads {
87 val gestureContext = contentScope.layoutState.currentTransition?.gestureContext
88 if (gestureContext != null) {
89 this.lastGestureContext = gestureContext
90 }
91 }
92 }
93
94 override val direction: InputDirection
95 get() = lastGestureContext.direction
96
97 override val dragOffset: Float
98 get() = lastGestureContext.dragOffset
99 }
100
101 private data class RevealContainerElement(val contentScope: ContentScope) :
102 ModifierNodeElement<RevealContainerNode>() {
createnull103 override fun create(): RevealContainerNode = RevealContainerNode(contentScope)
104
105 override fun update(node: RevealContainerNode) {
106 check(node.contentScope === contentScope)
107 }
108 }
109