1 /*
<lambda>null2  * Copyright 2022 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 androidx.compose.ui.layout
18 
19 import androidx.compose.runtime.Applier
20 import androidx.compose.runtime.Composable
21 import androidx.compose.runtime.ReusableComposeNode
22 import androidx.compose.runtime.remember
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.UiComposable
25 import androidx.compose.ui.geometry.Offset
26 import androidx.compose.ui.node.LayoutNode
27 import androidx.compose.ui.node.ModifierNodeElement
28 import androidx.compose.ui.node.NodeCoordinator
29 import androidx.compose.ui.platform.InspectorInfo
30 import androidx.compose.ui.unit.Constraints
31 import androidx.compose.ui.unit.IntSize
32 
33 /**
34  * [LookaheadScope] creates a scope in which all layouts will first determine their destination
35  * layout through a lookahead pass, followed by an _approach_ pass to run the measurement and
36  * placement approach defined in [approachLayout] or [ApproachLayoutModifierNode], in order to
37  * gradually reach the destination.
38  *
39  * Note: [LookaheadScope] does not introduce a new [Layout] to the [content] passed in. All the
40  * [Layout]s in the [content] will have the same parent as they would without [LookaheadScope].
41  *
42  * @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
43  * @param content The child composable to be laid out.
44  * @see ApproachLayoutModifierNode
45  * @see approachLayout
46  */
47 @UiComposable
48 @Composable
49 fun LookaheadScope(content: @Composable @UiComposable LookaheadScope.() -> Unit) {
50     val scope = remember { LookaheadScopeImpl() }
51     ReusableComposeNode<LayoutNode, Applier<Any>>(
52         factory = { LayoutNode(isVirtual = true) },
53         update = {
54             init { isVirtualLookaheadRoot = true }
55             set(scope) { scope ->
56                 // This internal lambda will be invoked during placement.
57                 scope.scopeCoordinates = { parent!!.innerCoordinator.coordinates }
58             }
59         },
60         content = { scope.content() }
61     )
62 }
63 
64 /**
65  * Creates an approach layout intended to help gradually approach the destination layout calculated
66  * in the lookahead pass. This can be particularly helpful when the destination layout is
67  * anticipated to change drastically and would consequently result in visual disruptions.
68  *
69  * In order to create a smooth approach, an interpolation (often through animations) can be used in
70  * [approachMeasure] to interpolate the measurement or placement from a previously recorded size
71  * and/or position to the destination/target size and/or position. The destination size is available
72  * in [ApproachMeasureScope] as [ApproachMeasureScope.lookaheadSize]. And the target position can
73  * also be acquired in [ApproachMeasureScope] during placement by using
74  * [LookaheadScope.localLookaheadPositionOf] with the layout's
75  * [Placeable.PlacementScope.coordinates]. The sample code below illustrates how that can be
76  * achieved.
77  *
78  * [isMeasurementApproachInProgress] signals whether the measurement is in progress of approaching
79  * destination size. It will be queried after the destination has been determined by the lookahead
80  * pass, before [approachMeasure] is invoked. The lookahead size is provided to
81  * [isMeasurementApproachInProgress] for convenience in deciding whether the destination size has
82  * been reached.
83  *
84  * [isMeasurementApproachInProgress] indicates whether the position is currently approaching
85  * destination defined by the lookahead, hence it's a signal to the system for whether additional
86  * approach placements are necessary. [isPlacementApproachInProgress] will be invoked after the
87  * destination position has been determined by lookahead pass, and before the placement phase in
88  * [approachMeasure].
89  *
90  * Once both [isMeasurementApproachInProgress] and [isPlacementApproachInProgress] return false, the
91  * system may skip approach pass until additional approach passes are necessary as indicated by
92  * [isMeasurementApproachInProgress] and [isPlacementApproachInProgress].
93  *
94  * **IMPORTANT**: It is important to be accurate in [isPlacementApproachInProgress] and
95  * [isMeasurementApproachInProgress]. A prolonged indication of incomplete approach will prevent the
96  * system from potentially skipping approach pass when possible.
97  *
98  * @sample androidx.compose.ui.samples.approachLayoutSample
99  * @see ApproachLayoutModifierNode
100  */
approachLayoutnull101 fun Modifier.approachLayout(
102     isMeasurementApproachInProgress: (lookaheadSize: IntSize) -> Boolean,
103     isPlacementApproachInProgress:
104         Placeable.PlacementScope.(lookaheadCoordinates: LayoutCoordinates) -> Boolean =
105         defaultPlacementApproachInProgress,
106     approachMeasure:
107         ApproachMeasureScope.(
108             measurable: Measurable,
109             constraints: Constraints,
110         ) -> MeasureResult,
111 ): Modifier =
112     this then
113         ApproachLayoutElement(
114             isMeasurementApproachInProgress = isMeasurementApproachInProgress,
115             isPlacementApproachInProgress = isPlacementApproachInProgress,
116             approachMeasure = approachMeasure
117         )
118 
119 private val defaultPlacementApproachInProgress:
120     Placeable.PlacementScope.(lookaheadCoordinates: LayoutCoordinates) -> Boolean =
121     {
122         false
123     }
124 
125 private class ApproachLayoutElement(
126     val approachMeasure:
127         ApproachMeasureScope.(
128             measurable: Measurable,
129             constraints: Constraints,
130         ) -> MeasureResult,
131     val isMeasurementApproachInProgress: (IntSize) -> Boolean,
132     val isPlacementApproachInProgress:
133         Placeable.PlacementScope.(lookaheadCoordinates: LayoutCoordinates) -> Boolean =
134         defaultPlacementApproachInProgress,
135 ) : ModifierNodeElement<ApproachLayoutModifierNodeImpl>() {
createnull136     override fun create() =
137         ApproachLayoutModifierNodeImpl(
138             approachMeasure,
139             isMeasurementApproachInProgress,
140             isPlacementApproachInProgress
141         )
142 
143     override fun update(node: ApproachLayoutModifierNodeImpl) {
144         node.measureBlock = approachMeasure
145         node.isMeasurementApproachInProgress = isMeasurementApproachInProgress
146         node.isPlacementApproachInProgress = isPlacementApproachInProgress
147     }
148 
inspectablePropertiesnull149     override fun InspectorInfo.inspectableProperties() {
150         name = "approachLayout"
151         properties["approachMeasure"] = approachMeasure
152         properties["isMeasurementApproachInProgress"] = isMeasurementApproachInProgress
153         properties["isPlacementApproachInProgress"] = isPlacementApproachInProgress
154     }
155 
equalsnull156     override fun equals(other: Any?): Boolean {
157         if (this === other) return true
158         if (other !is ApproachLayoutElement) return false
159 
160         if (approachMeasure !== other.approachMeasure) return false
161         if (isMeasurementApproachInProgress !== other.isMeasurementApproachInProgress) return false
162         if (isPlacementApproachInProgress !== other.isPlacementApproachInProgress) return false
163 
164         return true
165     }
166 
hashCodenull167     override fun hashCode(): Int {
168         var result = approachMeasure.hashCode()
169         result = 31 * result + isMeasurementApproachInProgress.hashCode()
170         result = 31 * result + isPlacementApproachInProgress.hashCode()
171         return result
172     }
173 }
174 
175 private class ApproachLayoutModifierNodeImpl(
176     var measureBlock:
177         ApproachMeasureScope.(
178             measurable: Measurable,
179             constraints: Constraints,
180         ) -> MeasureResult,
181     var isMeasurementApproachInProgress: (IntSize) -> Boolean,
182     var isPlacementApproachInProgress: Placeable.PlacementScope.(LayoutCoordinates) -> Boolean,
183 ) : ApproachLayoutModifierNode, Modifier.Node() {
isMeasurementApproachInProgressnull184     override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
185         return isMeasurementApproachInProgress.invoke(lookaheadSize)
186     }
187 
isPlacementApproachInProgressnull188     override fun Placeable.PlacementScope.isPlacementApproachInProgress(
189         lookaheadCoordinates: LayoutCoordinates
190     ): Boolean {
191         return isPlacementApproachInProgress.invoke(this, lookaheadCoordinates)
192     }
193 
approachMeasurenull194     override fun ApproachMeasureScope.approachMeasure(
195         measurable: Measurable,
196         constraints: Constraints
197     ): MeasureResult {
198         return measureBlock(measurable, constraints)
199     }
200 }
201 
202 /**
203  * [LookaheadScope] provides a receiver scope for all (direct and indirect) child layouts in
204  * [LookaheadScope]. This receiver scope allows access to [lookaheadScopeCoordinates] from any
205  * child's [Placeable.PlacementScope]. It also allows any child to convert [LayoutCoordinates]
206  * (which can be retrieved in [Placeable.PlacementScope]) to [LayoutCoordinates] in lookahead
207  * coordinate space using [toLookaheadCoordinates].
208  *
209  * @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
210  */
211 interface LookaheadScope {
212     /**
213      * Converts a [LayoutCoordinates] into a [LayoutCoordinates] in the Lookahead coordinate space.
214      * This can be used for layouts within [LookaheadScope].
215      */
LayoutCoordinatesnull216     fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates
217 
218     /**
219      * Returns the [LayoutCoordinates] of the [LookaheadScope]. This is only accessible from
220      * [Placeable.PlacementScope] (i.e. during placement time).
221      *
222      * Note: The returned coordinates is **not** coordinates in the lookahead coordinate space. If
223      * the lookahead coordinates of the lookaheadScope is needed, suggest converting the returned
224      * coordinates using [toLookaheadCoordinates].
225      */
226     val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
227 
228     /**
229      * Converts [relativeToSource] in [sourceCoordinates]'s lookahead coordinate space into local
230      * lookahead coordinates. This is a convenient method for 1) converting both [this] coordinates
231      * and [sourceCoordinates] into lookahead space coordinates using [toLookaheadCoordinates],
232      * and 2) invoking [LayoutCoordinates.localPositionOf] with the converted coordinates.
233      *
234      * For layouts where [LayoutCoordinates.introducesMotionFrameOfReference] returns `true` (placed
235      * under [Placeable.PlacementScope.withMotionFrameOfReferencePlacement]) you may pass
236      * [includeMotionFrameOfReference] as `false` to get their position while excluding the
237      * additional Offset.
238      */
239     fun LayoutCoordinates.localLookaheadPositionOf(
240         sourceCoordinates: LayoutCoordinates,
241         relativeToSource: Offset = Offset.Zero,
242         includeMotionFrameOfReference: Boolean = true,
243     ): Offset =
244         localLookaheadPositionOf(
245             coordinates = this,
246             sourceCoordinates = sourceCoordinates,
247             relativeToSource = relativeToSource,
248             includeMotionFrameOfReference = includeMotionFrameOfReference
249         )
250 }
251 
252 /** Internal implementation to handle [LookaheadScope.localLookaheadPositionOf]. */
253 internal fun LookaheadScope.localLookaheadPositionOf(
254     coordinates: LayoutCoordinates,
255     sourceCoordinates: LayoutCoordinates,
256     relativeToSource: Offset,
257     includeMotionFrameOfReference: Boolean
258 ): Offset {
259     val lookaheadCoords = coordinates.toLookaheadCoordinates()
260     val source = sourceCoordinates.toLookaheadCoordinates()
261 
262     return if (lookaheadCoords is LookaheadLayoutCoordinates) {
263         lookaheadCoords.localPositionOf(
264             sourceCoordinates = source,
265             relativeToSource = relativeToSource,
266             includeMotionFrameOfReference = includeMotionFrameOfReference
267         )
268     } else if (source is LookaheadLayoutCoordinates) {
269         // Relative from source, so we take its negative position
270         -source.localPositionOf(
271             sourceCoordinates = lookaheadCoords,
272             relativeToSource = relativeToSource,
273             includeMotionFrameOfReference = includeMotionFrameOfReference
274         )
275     } else {
276         lookaheadCoords.localPositionOf(
277             sourceCoordinates = lookaheadCoords,
278             relativeToSource = relativeToSource,
279             includeMotionFrameOfReference = includeMotionFrameOfReference
280         )
281     }
282 }
283 
284 internal class LookaheadScopeImpl(var scopeCoordinates: (() -> LayoutCoordinates)? = null) :
285     LookaheadScope {
toLookaheadCoordinatesnull286     override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates {
287         return this as? LookaheadLayoutCoordinates
288             ?: (this as NodeCoordinator).let {
289                 // If the coordinator has no lookahead delegate. Its
290                 // lookahead coords is the same as its coords
291                 it.lookaheadDelegate?.lookaheadLayoutCoordinates ?: it
292             }
293     }
294 
295     override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
296         get() = scopeCoordinates!!()
297 }
298