1 /*
<lambda>null2  * Copyright 2024 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.ui.node.LayoutModifierNode
20 import androidx.compose.ui.node.NodeMeasuringIntrinsics
21 import androidx.compose.ui.unit.Constraints
22 import androidx.compose.ui.unit.IntSize
23 
24 /**
25  * [ApproachLayoutModifierNode] is designed to support gradually approaching the destination layout
26  * calculated in the lookahead pass. This can be particularly helpful when the destination layout is
27  * anticipated to change drastically and would consequently result in visual disruptions.
28  *
29  * In order to create a smooth approach, an interpolation (often through animations) can be used in
30  * [approachMeasure] to interpolate the measurement or placement from a previously recorded size
31  * and/or position to the destination/target size and/or position. The destination size is available
32  * in [ApproachMeasureScope] as [ApproachMeasureScope.lookaheadSize]. And the target position can
33  * also be acquired in [ApproachMeasureScope] during placement by using
34  * [LookaheadScope.localLookaheadPositionOf] with the layout's
35  * [Placeable.PlacementScope.coordinates]. The sample code below illustrates how that can be
36  * achieved.
37  *
38  * During the lookahead pass, [measure] will be invoked. By default [measure] simply passes the
39  * incoming constraints to its child, and returns the child measure result to parent without any
40  * modification. The default behavior for [measure] is simply a pass through of constraints and
41  * measure results without modification. This can be overridden as needed. [approachMeasure] will be
42  * invoked during the approach pass after lookahead.
43  *
44  * [isMeasurementApproachInProgress] signals whether the measurement is in progress of approaching
45  * destination size. It will be queried after the destination has been determined by the lookahead
46  * pass, before [approachMeasure] is invoked. The lookahead size is provided to
47  * [isMeasurementApproachInProgress] for convenience in deciding whether the destination size has
48  * been reached.
49  *
50  * [isPlacementApproachInProgress] indicates whether the position is actively approaching
51  * destination defined by the lookahead, hence it's a signal to the system for whether additional
52  * approach placements are necessary. [isPlacementApproachInProgress] will be invoked after the
53  * destination position has been determined by lookahead pass, and before the placement phase in
54  * [approachMeasure].
55  *
56  * **IMPORTANT**: When both [isMeasurementApproachInProgress] and [isPlacementApproachInProgress]
57  * become false, the approach is considered complete. Approach pass will subsequently snap the
58  * measurement and placement to lookahead measurement and placement. Once approach is complete,
59  * [approachMeasure] may never be invoked until either [isMeasurementApproachInProgress] or
60  * [isPlacementApproachInProgress] becomes true again. Therefore it is important to ensure
61  * [approachMeasure] and [measure] result in the same measurement and placement when the approach is
62  * complete. Otherwise, there may be visual discontinuity when we snap the measurement and placement
63  * to lookahead.
64  *
65  * It is important to be accurate in [isPlacementApproachInProgress] and
66  * [isMeasurementApproachInProgress]. A prolonged indication of incomplete approach will prevent the
67  * system from potentially skipping approach pass when possible.
68  *
69  * @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
70  */
71 interface ApproachLayoutModifierNode : LayoutModifierNode {
72     /**
73      * [isMeasurementApproachInProgress] signals whether the measurement is currently approaching
74      * destination size. It will be queried after the destination has been determined by the
75      * lookahead pass, before [approachMeasure] is invoked. The lookahead size is provided to
76      * [isMeasurementApproachInProgress] for convenience in deciding whether the destination size
77      * has been reached.
78      *
79      * Note: It is important to be accurate in [isPlacementApproachInProgress] and
80      * [isMeasurementApproachInProgress]. A prolonged indication of incomplete approach will prevent
81      * the system from potentially skipping approach pass when possible.
82      */
83     fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean
84 
85     /**
86      * [isPlacementApproachInProgress] indicates whether the position is approaching destination
87      * defined by the lookahead, hence it's a signal to the system for whether additional approach
88      * placements are necessary. [isPlacementApproachInProgress] will be invoked after the
89      * destination position has been determined by lookahead pass, and before the placement phase in
90      * [approachMeasure].
91      *
92      * Note: It is important to be accurate in [isPlacementApproachInProgress] and
93      * [isMeasurementApproachInProgress]. A prolonged indication of incomplete approach will prevent
94      * the system from potentially skipping approach pass when possible.
95      *
96      * By default, [isPlacementApproachInProgress] returns false.
97      */
98     fun Placeable.PlacementScope.isPlacementApproachInProgress(
99         lookaheadCoordinates: LayoutCoordinates
100     ): Boolean {
101         return false
102     }
103 
104     override fun MeasureScope.measure(
105         measurable: Measurable,
106         constraints: Constraints
107     ): MeasureResult = measurable.measure(constraints).run { layout(width, height) { place(0, 0) } }
108 
109     /**
110      * [approachMeasure] defines how the measurement and placement of the layout approach the
111      * destination size and position. In order to achieve a smooth approach from the current size
112      * and position to the destination, an interpolation (often through animations) can be used in
113      * [approachMeasure] to interpolate the measurement or placement from a previously recorded size
114      * and position to the destination/target size and position. The destination size is available
115      * in [ApproachMeasureScope] as [ApproachMeasureScope.lookaheadSize]. And the target position
116      * can also be acquired in [ApproachMeasureScope] during placement by using
117      * [LookaheadScope.localLookaheadPositionOf] with the layout's
118      * [Placeable.PlacementScope.coordinates]. Please see sample code below for how that can be
119      * achieved.
120      *
121      * Note: [approachMeasure] is only guaranteed to be invoked when either
122      * [isMeasurementApproachInProgress] or [isMeasurementApproachInProgress] is true. Otherwise,
123      * the system will consider the approach complete (i.e. destination reached) and may skip the
124      * approach pass when possible.
125      *
126      * @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
127      */
128     fun ApproachMeasureScope.approachMeasure(
129         measurable: Measurable,
130         constraints: Constraints
131     ): MeasureResult
132 
133     /** The function used to calculate minIntrinsicWidth for the approach pass changes. */
134     fun ApproachIntrinsicMeasureScope.minApproachIntrinsicWidth(
135         measurable: IntrinsicMeasurable,
136         height: Int
137     ): Int =
138         if (node.coordinator!!.lookaheadDelegate!!.hasMeasureResult) {
139             // Only invoke approachMeasure when the node has been measured in lookahead, otherwise
140             // skip the approach measure as it might access lookahead size/constraints, neither of
141             // which would be set until lookahead measure. In that case, fall back to returning
142             // child layout's intrinsic size.
143             NodeMeasuringIntrinsics.minWidth(
144                 NodeMeasuringIntrinsics.ApproachMeasureBlock { intrinsicMeasurable, constraints ->
145                     approachMeasure(intrinsicMeasurable, constraints)
146                 },
147                 this,
148                 measurable,
149                 height
150             )
151         } else {
152             measurable.minIntrinsicWidth(height)
153         }
154 
155     /** The function used to calculate minIntrinsicHeight for the approach pass changes. */
156     fun ApproachIntrinsicMeasureScope.minApproachIntrinsicHeight(
157         measurable: IntrinsicMeasurable,
158         width: Int
159     ): Int =
160         if (node.coordinator!!.lookaheadDelegate!!.hasMeasureResult) {
161             // Only invoke approachMeasure when the node has been measured in lookahead, otherwise
162             // skip the approach measure as it might access lookahead size/constraints, neither of
163             // which would be set until lookahead measure. In that case, fall back to returning
164             // child layout's intrinsic size.
165             NodeMeasuringIntrinsics.minHeight(
166                 NodeMeasuringIntrinsics.ApproachMeasureBlock { intrinsicMeasurable, constraints ->
167                     approachMeasure(intrinsicMeasurable, constraints)
168                 },
169                 this,
170                 measurable,
171                 width
172             )
173         } else {
174             measurable.minIntrinsicHeight(width)
175         }
176 
177     /** The function used to calculate maxIntrinsicWidth for the approach pass changes. */
178     fun ApproachIntrinsicMeasureScope.maxApproachIntrinsicWidth(
179         measurable: IntrinsicMeasurable,
180         height: Int
181     ): Int =
182         if (node.coordinator!!.lookaheadDelegate!!.hasMeasureResult) {
183             // Only invoke approachMeasure when the node has been measured in lookahead, otherwise
184             // skip the approach measure as it might access lookahead size/constraints, neither of
185             // which would be set until lookahead measure. In that case, fall back to returning
186             // child layout's intrinsic size.
187             NodeMeasuringIntrinsics.maxWidth(
188                 NodeMeasuringIntrinsics.ApproachMeasureBlock { intrinsicMeasurable, constraints ->
189                     approachMeasure(intrinsicMeasurable, constraints)
190                 },
191                 this,
192                 measurable,
193                 height
194             )
195         } else {
196             measurable.maxIntrinsicWidth(height)
197         }
198 
199     /** The function used to calculate maxIntrinsicHeight for the approach pass changes. */
200     fun ApproachIntrinsicMeasureScope.maxApproachIntrinsicHeight(
201         measurable: IntrinsicMeasurable,
202         width: Int
203     ): Int =
204         if (node.coordinator!!.lookaheadDelegate!!.hasMeasureResult) {
205             // Only invoke approachMeasure when the node has been measured in lookahead, otherwise
206             // skip the approach measure as it might access lookahead size/constraints, neither of
207             // which would be set until lookahead measure. In that case, fall back to returning
208             // child layout's intrinsic size.
209             NodeMeasuringIntrinsics.maxHeight(
210                 NodeMeasuringIntrinsics.ApproachMeasureBlock { intrinsicMeasurable, constraints ->
211                     approachMeasure(intrinsicMeasurable, constraints)
212                 },
213                 this,
214                 measurable,
215                 width
216             )
217         } else {
218             measurable.maxIntrinsicHeight(width)
219         }
220 }
221