1 /*
2  * Copyright 2020 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.Modifier
20 import androidx.compose.ui.graphics.GraphicsLayerScope
21 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
22 import androidx.compose.ui.node.LayoutModifierNode
23 import androidx.compose.ui.node.ModifierNodeElement
24 import androidx.compose.ui.platform.InspectorInfo
25 import androidx.compose.ui.unit.Constraints
26 import androidx.compose.ui.unit.IntOffset
27 import androidx.compose.ui.unit.IntSize
28 
29 /**
30  * A [Modifier.Element] that changes how its wrapped content is measured and laid out. It has the
31  * same measurement and layout functionality as the [androidx.compose.ui.layout.Layout] component,
32  * while wrapping exactly one layout due to it being a modifier. In contrast, the
33  * [androidx.compose.ui.layout.Layout] component is used to define the layout behavior of multiple
34  * children.
35  *
36  * @sample androidx.compose.ui.samples.LayoutModifierSample
37  * @see androidx.compose.ui.layout.Layout
38  */
39 @JvmDefaultWithCompatibility
40 interface LayoutModifier : Modifier.Element {
41     /**
42      * The function used to measure the modifier. The [measurable] corresponds to the wrapped
43      * content, and it can be measured with the desired constraints according to the logic of the
44      * [LayoutModifier]. The modifier needs to choose its own size, which can depend on the size
45      * chosen by the wrapped content (the obtained [Placeable]), if the wrapped content was
46      * measured. The size needs to be returned as part of a [MeasureResult], alongside the placement
47      * logic of the [Placeable], which defines how the wrapped content should be positioned inside
48      * the [LayoutModifier]. A convenient way to create the [MeasureResult] is to use the
49      * [MeasureScope.layout] factory function.
50      *
51      * A [LayoutModifier] uses the same measurement and layout concepts and principles as a
52      * [Layout], the only difference is that they apply to exactly one child. For a more detailed
53      * explanation of measurement and layout, see [MeasurePolicy].
54      */
MeasureScopenull55     fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult
56 
57     /** The function used to calculate [IntrinsicMeasurable.minIntrinsicWidth]. */
58     fun IntrinsicMeasureScope.minIntrinsicWidth(measurable: IntrinsicMeasurable, height: Int): Int =
59         MeasuringIntrinsics.minWidth(this@LayoutModifier, this, measurable, height)
60 
61     /** The lambda used to calculate [IntrinsicMeasurable.minIntrinsicHeight]. */
62     fun IntrinsicMeasureScope.minIntrinsicHeight(measurable: IntrinsicMeasurable, width: Int): Int =
63         MeasuringIntrinsics.minHeight(this@LayoutModifier, this, measurable, width)
64 
65     /** The function used to calculate [IntrinsicMeasurable.maxIntrinsicWidth]. */
66     fun IntrinsicMeasureScope.maxIntrinsicWidth(measurable: IntrinsicMeasurable, height: Int): Int =
67         MeasuringIntrinsics.maxWidth(this@LayoutModifier, this, measurable, height)
68 
69     /** The lambda used to calculate [IntrinsicMeasurable.maxIntrinsicHeight]. */
70     fun IntrinsicMeasureScope.maxIntrinsicHeight(measurable: IntrinsicMeasurable, width: Int): Int =
71         MeasuringIntrinsics.maxHeight(this@LayoutModifier, this, measurable, width)
72 }
73 
74 // TODO(popam): deduplicate from the copy-pasted logic of Layout.kt without making it public
75 private object MeasuringIntrinsics {
76     fun minWidth(
77         modifier: LayoutModifier,
78         intrinsicMeasureScope: IntrinsicMeasureScope,
79         intrinsicMeasurable: IntrinsicMeasurable,
80         h: Int
81     ): Int {
82         val measurable =
83             DefaultIntrinsicMeasurable(
84                 intrinsicMeasurable,
85                 IntrinsicMinMax.Min,
86                 IntrinsicWidthHeight.Width
87             )
88         val constraints = Constraints(maxHeight = h)
89         val layoutResult =
90             with(modifier) {
91                 IntrinsicsMeasureScope(intrinsicMeasureScope, intrinsicMeasureScope.layoutDirection)
92                     .measure(measurable, constraints)
93             }
94         return layoutResult.width
95     }
96 
97     fun minHeight(
98         modifier: LayoutModifier,
99         intrinsicMeasureScope: IntrinsicMeasureScope,
100         intrinsicMeasurable: IntrinsicMeasurable,
101         w: Int
102     ): Int {
103         val measurable =
104             DefaultIntrinsicMeasurable(
105                 intrinsicMeasurable,
106                 IntrinsicMinMax.Min,
107                 IntrinsicWidthHeight.Height
108             )
109         val constraints = Constraints(maxWidth = w)
110         val layoutResult =
111             with(modifier) {
112                 IntrinsicsMeasureScope(intrinsicMeasureScope, intrinsicMeasureScope.layoutDirection)
113                     .measure(measurable, constraints)
114             }
115         return layoutResult.height
116     }
117 
118     fun maxWidth(
119         modifier: LayoutModifier,
120         intrinsicMeasureScope: IntrinsicMeasureScope,
121         intrinsicMeasurable: IntrinsicMeasurable,
122         h: Int
123     ): Int {
124         val measurable =
125             DefaultIntrinsicMeasurable(
126                 intrinsicMeasurable,
127                 IntrinsicMinMax.Max,
128                 IntrinsicWidthHeight.Width
129             )
130         val constraints = Constraints(maxHeight = h)
131         val layoutResult =
132             with(modifier) {
133                 IntrinsicsMeasureScope(intrinsicMeasureScope, intrinsicMeasureScope.layoutDirection)
134                     .measure(measurable, constraints)
135             }
136         return layoutResult.width
137     }
138 
139     fun maxHeight(
140         modifier: LayoutModifier,
141         intrinsicMeasureScope: IntrinsicMeasureScope,
142         intrinsicMeasurable: IntrinsicMeasurable,
143         w: Int
144     ): Int {
145         val measurable =
146             DefaultIntrinsicMeasurable(
147                 intrinsicMeasurable,
148                 IntrinsicMinMax.Max,
149                 IntrinsicWidthHeight.Height
150             )
151         val constraints = Constraints(maxWidth = w)
152         val layoutResult =
153             with(modifier) {
154                 IntrinsicsMeasureScope(intrinsicMeasureScope, intrinsicMeasureScope.layoutDirection)
155                     .measure(measurable, constraints)
156             }
157         return layoutResult.height
158     }
159 
160     private class DefaultIntrinsicMeasurable(
161         val measurable: IntrinsicMeasurable,
162         val minMax: IntrinsicMinMax,
163         val widthHeight: IntrinsicWidthHeight
164     ) : Measurable {
165         override val parentData: Any?
166             get() = measurable.parentData
167 
168         override fun measure(constraints: Constraints): Placeable {
169             if (widthHeight == IntrinsicWidthHeight.Width) {
170                 val width =
171                     if (minMax == IntrinsicMinMax.Max) {
172                         measurable.maxIntrinsicWidth(constraints.maxHeight)
173                     } else {
174                         measurable.minIntrinsicWidth(constraints.maxHeight)
175                     }
176                 val height =
177                     if (constraints.hasBoundedHeight) constraints.maxHeight else LargeDimension
178                 return EmptyPlaceable(width, height)
179             }
180             val height =
181                 if (minMax == IntrinsicMinMax.Max) {
182                     measurable.maxIntrinsicHeight(constraints.maxWidth)
183                 } else {
184                     measurable.minIntrinsicHeight(constraints.maxWidth)
185                 }
186             val width = if (constraints.hasBoundedWidth) constraints.maxWidth else LargeDimension
187             return EmptyPlaceable(width, height)
188         }
189 
190         override fun minIntrinsicWidth(height: Int): Int {
191             return measurable.minIntrinsicWidth(height)
192         }
193 
194         override fun maxIntrinsicWidth(height: Int): Int {
195             return measurable.maxIntrinsicWidth(height)
196         }
197 
198         override fun minIntrinsicHeight(width: Int): Int {
199             return measurable.minIntrinsicHeight(width)
200         }
201 
202         override fun maxIntrinsicHeight(width: Int): Int {
203             return measurable.maxIntrinsicHeight(width)
204         }
205     }
206 
207     private class EmptyPlaceable(width: Int, height: Int) : Placeable() {
208         init {
209             measuredSize = IntSize(width, height)
210         }
211 
212         override fun get(alignmentLine: AlignmentLine): Int = AlignmentLine.Unspecified
213 
214         override fun placeAt(
215             position: IntOffset,
216             zIndex: Float,
217             layerBlock: (GraphicsLayerScope.() -> Unit)?
218         ) {}
219     }
220 
221     private enum class IntrinsicMinMax {
222         Min,
223         Max
224     }
225 
226     private enum class IntrinsicWidthHeight {
227         Width,
228         Height
229     }
230 }
231 
232 /**
233  * Creates a [LayoutModifier] that allows changing how the wrapped element is measured and laid out.
234  *
235  * This is a convenience API of creating a custom [LayoutModifier] modifier, without having to
236  * create a class or an object that implements the [LayoutModifier] interface. The intrinsic
237  * measurements follow the default logic provided by the [LayoutModifier].
238  *
239  * Example usage:
240  *
241  * @sample androidx.compose.ui.samples.ConvenienceLayoutModifierSample
242  * @see androidx.compose.ui.layout.LayoutModifier
243  */
layoutnull244 fun Modifier.layout(measure: MeasureScope.(Measurable, Constraints) -> MeasureResult) =
245     this then LayoutElement(measure)
246 
247 private class LayoutElement(val measure: MeasureScope.(Measurable, Constraints) -> MeasureResult) :
248     ModifierNodeElement<LayoutModifierImpl>() {
249     override fun create() = LayoutModifierImpl(measure)
250 
251     override fun update(node: LayoutModifierImpl) {
252         node.measureBlock = measure
253     }
254 
255     override fun InspectorInfo.inspectableProperties() {
256         name = "layout"
257         properties["measure"] = measure
258     }
259 
260     override fun equals(other: Any?): Boolean {
261         if (this === other) return true
262         if (other !is LayoutElement) return false
263 
264         if (measure !== other.measure) return false
265 
266         return true
267     }
268 
269     override fun hashCode(): Int {
270         return measure.hashCode()
271     }
272 }
273 
274 internal class LayoutModifierImpl(
275     var measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult
276 ) : LayoutModifierNode, Modifier.Node() {
measurenull277     override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints) =
278         measureBlock(measurable, constraints)
279 
280     override fun toString(): String {
281         return "LayoutModifierImpl(measureBlock=$measureBlock)"
282     }
283 }
284