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