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