• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.util.Log
20 import androidx.compose.runtime.Composable
21 import androidx.compose.runtime.remember
22 import androidx.compose.ui.Modifier
23 import androidx.compose.ui.geometry.Rect
24 import androidx.compose.ui.graphics.CompositingStrategy
25 import androidx.compose.ui.layout.ApproachLayoutModifierNode
26 import androidx.compose.ui.layout.ApproachMeasureScope
27 import androidx.compose.ui.layout.LayoutCoordinates
28 import androidx.compose.ui.layout.Measurable
29 import androidx.compose.ui.layout.MeasureResult
30 import androidx.compose.ui.layout.MeasureScope
31 import androidx.compose.ui.layout.Placeable
32 import androidx.compose.ui.node.ModifierNodeElement
33 import androidx.compose.ui.node.findNearestAncestor
34 import androidx.compose.ui.node.requireDensity
35 import androidx.compose.ui.node.requireLayoutCoordinates
36 import androidx.compose.ui.unit.Constraints
37 import androidx.compose.ui.unit.Dp
38 import androidx.compose.ui.unit.IntOffset
39 import androidx.compose.ui.unit.IntSize
40 import androidx.compose.ui.unit.dp
41 import androidx.compose.ui.unit.toSize
42 import com.android.mechanics.MotionValue
43 import com.android.mechanics.debug.findMotionValueDebugger
44 import com.android.mechanics.demo.staging.behavior.reveal.RevealContainerNode.Companion.TRAVERSAL_NODE_KEY
45 import com.android.mechanics.demo.staging.defaultEffectSpring
46 import com.android.mechanics.spec.DirectionalMotionSpec
47 import com.android.mechanics.spec.Guarantee
48 import com.android.mechanics.spec.Mapping
49 import com.android.mechanics.spec.MotionSpec
50 import com.android.mechanics.spec.builder
51 import com.android.mechanics.spec.reverseBuilder
52 import com.android.mechanics.spring.SpringParameters
53 import kotlinx.coroutines.DisposableHandle
54 import kotlinx.coroutines.launch
55 
56 @Composable
rememberFadeContentRevealSpecnull57 fun rememberFadeContentRevealSpec(
58     showSpring: SpringParameters = defaultEffectSpring(),
59     hideSpring: SpringParameters = defaultEffectSpring(),
60     showDelta: Dp = 0.dp,
61     hideDelta: Dp = 0.dp,
62     showGuarantee: Guarantee = Guarantee.None,
63     hideGuarantee: Guarantee = Guarantee.None,
64 ): FadeContentRevealSpec {
65     return remember(showSpring, hideSpring, showGuarantee, hideGuarantee) {
66         FadeContentRevealSpec(
67             showSpring,
68             hideSpring,
69             showDelta,
70             hideDelta,
71             showGuarantee,
72             hideGuarantee,
73         )
74     }
75 }
76 
77 @Composable
fadeRevealnull78 fun Modifier.fadeReveal(
79     spec: FadeContentRevealSpec = rememberFadeContentRevealSpec(),
80     debug: Boolean = false,
81     label: String? = null,
82 ): Modifier = this.then(FadeContentRevealElement(spec, debug, label))
83 
84 data class FadeContentRevealSpec(
85     val showSpring: SpringParameters,
86     val hideSpring: SpringParameters,
87     val showDelta: Dp,
88     val hideDelta: Dp,
89     val showGuarantee: Guarantee,
90     val hideGuarantee: Guarantee,
91 )
92 
93 internal class FadeContentRevealNode(
94     private var spec: FadeContentRevealSpec,
95     private val debug: Boolean,
96     private val label: String?,
97 ) : Modifier.Node(), ApproachLayoutModifierNode {
98 
99     private class AttachedState(
100         val revealContainerNode: RevealContainerNode,
101         val alphaValue: MotionValue,
102         val debugDisposer: DisposableHandle?,
103     )
104 
105     private var attachedState: AttachedState? = null
106 
107     fun updateSpec(spec: FadeContentRevealSpec) {
108         this.spec = spec
109         attachedState?.updateSpec(lookaheadTargetBounds)
110     }
111 
112     private var lookaheadTargetBounds = Rect.Zero
113 
114     private fun AttachedState.updateSpec(lookaheadBounds: Rect) {
115         with(requireDensity()) {
116             val showSpec =
117                 DirectionalMotionSpec.builder(
118                         initialMapping = Mapping.Zero,
119                         defaultSpring = spec.showSpring,
120                     )
121                     .toBreakpoint(atPosition = lookaheadBounds.bottom + spec.showDelta.toPx())
122                     .continueWith(Mapping.One, guarantee = spec.showGuarantee)
123                     .complete()
124 
125             val hideSpec =
126                 DirectionalMotionSpec.reverseBuilder(
127                         initialMapping = Mapping.One,
128                         defaultSpring = spec.hideSpring,
129                     )
130                     .toBreakpoint(atPosition = lookaheadBounds.bottom + spec.hideDelta.toPx())
131                     .continueWith(Mapping.Zero, guarantee = spec.hideGuarantee)
132                     .complete()
133 
134             alphaValue.spec = MotionSpec(maxDirection = showSpec, minDirection = hideSpec)
135         }
136     }
137 
138     override fun onAttach() {
139         val revealContainerNode =
140             checkNotNull(findNearestAncestor(TRAVERSAL_NODE_KEY)) as RevealContainerNode
141 
142         val alphaValue =
143             MotionValue(
144                 currentInput = revealContainerNode::containerHeight,
145                 gestureContext = revealContainerNode,
146                 label = "FadeReveal($label)::alpha",
147             )
148 
149         var debugDisposer: DisposableHandle? = null
150         if (debug) {
151             val motionValueDebugger = findMotionValueDebugger()
152             if (motionValueDebugger != null) {
153                 debugDisposer = motionValueDebugger.register(alphaValue)
154             } else {
155                 Log.w(TAG, "Debugging requested, but debugger not found.")
156             }
157         }
158 
159         attachedState = AttachedState(revealContainerNode, alphaValue, debugDisposer)
160         coroutineScope.launch { alphaValue.keepRunning() }
161     }
162 
163     override fun onDetach() {
164         attachedState?.debugDisposer?.dispose()
165         attachedState = null
166     }
167 
168     override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
169         with(checkNotNull(attachedState)) {
170             return revealContainerNode.contentScope.layoutState.currentTransition != null ||
171                 !alphaValue.isStable
172         }
173     }
174 
175     override fun Placeable.PlacementScope.isPlacementApproachInProgress(
176         lookaheadCoordinates: LayoutCoordinates
177     ): Boolean {
178         with(checkNotNull(attachedState)) {
179             return revealContainerNode.contentScope.layoutState.currentTransition != null ||
180                 !alphaValue.isStable
181         }
182     }
183 
184     override fun MeasureScope.measure(
185         measurable: Measurable,
186         constraints: Constraints,
187     ): MeasureResult {
188         val placeable = measurable.measure(constraints)
189         return layout(placeable.width, placeable.height) {
190             if (isLookingAhead && coordinates != null) {
191 
192                 with(checkNotNull(attachedState)) {
193                     lookaheadTargetBounds =
194                         Rect(
195                             with(revealContainerNode.contentScope.lookaheadScope) {
196                                 revealContainerNode
197                                     .requireLayoutCoordinates()
198                                     .localLookaheadPositionOf(coordinates!!)
199                             },
200                             coordinates!!.size.toSize(),
201                         )
202                     updateSpec(lookaheadTargetBounds)
203                 }
204             }
205 
206             placeable.place(0, 0)
207         }
208     }
209 
210     override fun ApproachMeasureScope.approachMeasure(
211         measurable: Measurable,
212         constraints: Constraints,
213     ): MeasureResult {
214         return measurable.measure(constraints).run {
215             layout(width, height) {
216                 val revealAlpha = checkNotNull(attachedState).alphaValue.output
217 
218                 if (revealAlpha < 1) {
219                     placeWithLayer(IntOffset.Zero) {
220                         alpha = revealAlpha.coerceAtLeast(0f)
221                         compositingStrategy = CompositingStrategy.ModulateAlpha
222                     }
223                 } else {
224                     place(IntOffset.Zero)
225                 }
226             }
227         }
228     }
229 
230     companion object {
231         const val TAG = "FadeReveal"
232     }
233 }
234 
235 internal data class FadeContentRevealElement(
236     val spec: FadeContentRevealSpec,
237     val debug: Boolean,
238     val label: String?,
239 ) : ModifierNodeElement<FadeContentRevealNode>() {
createnull240     override fun create(): FadeContentRevealNode = FadeContentRevealNode(spec, debug, label)
241 
242     override fun update(node: FadeContentRevealNode) {
243         node.updateSpec(spec)
244     }
245 }
246