1 /*
2 * Copyright 2019 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.foundation.layout
18
19 import androidx.compose.foundation.layout.PaddingValues.Absolute
20 import androidx.compose.foundation.layout.internal.requirePrecondition
21 import androidx.compose.runtime.Immutable
22 import androidx.compose.runtime.Stable
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.layout.Measurable
25 import androidx.compose.ui.layout.MeasureResult
26 import androidx.compose.ui.layout.MeasureScope
27 import androidx.compose.ui.node.LayoutModifierNode
28 import androidx.compose.ui.node.ModifierNodeElement
29 import androidx.compose.ui.platform.InspectorInfo
30 import androidx.compose.ui.unit.Constraints
31 import androidx.compose.ui.unit.Dp
32 import androidx.compose.ui.unit.LayoutDirection
33 import androidx.compose.ui.unit.constrainHeight
34 import androidx.compose.ui.unit.constrainWidth
35 import androidx.compose.ui.unit.dp
36 import androidx.compose.ui.unit.isUnspecified
37 import androidx.compose.ui.unit.offset
38
39 /**
40 * Apply additional space along each edge of the content in [Dp]: [start], [top], [end] and
41 * [bottom]. The start and end edges will be determined by the current [LayoutDirection]. Padding is
42 * applied before content measurement and takes precedence; content may only be as large as the
43 * remaining space.
44 *
45 * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
46 * [Modifier.offset].
47 *
48 * Example usage:
49 *
50 * @sample androidx.compose.foundation.layout.samples.PaddingModifier
51 */
52 @Stable
paddingnull53 fun Modifier.padding(start: Dp = 0.dp, top: Dp = 0.dp, end: Dp = 0.dp, bottom: Dp = 0.dp) =
54 this then
55 PaddingElement(
56 start = start,
57 top = top,
58 end = end,
59 bottom = bottom,
60 rtlAware = true,
61 inspectorInfo = {
62 name = "padding"
63 properties["start"] = start
64 properties["top"] = top
65 properties["end"] = end
66 properties["bottom"] = bottom
67 }
68 )
69
70 /**
71 * Apply [horizontal] dp space along the left and right edges of the content, and [vertical] dp
72 * space along the top and bottom edges. Padding is applied before content measurement and takes
73 * precedence; content may only be as large as the remaining space.
74 *
75 * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
76 * [Modifier.offset].
77 *
78 * Example usage:
79 *
80 * @sample androidx.compose.foundation.layout.samples.SymmetricPaddingModifier
81 */
82 @Stable
paddingnull83 fun Modifier.padding(horizontal: Dp = 0.dp, vertical: Dp = 0.dp) =
84 this then
85 PaddingElement(
86 start = horizontal,
87 top = vertical,
88 end = horizontal,
89 bottom = vertical,
90 rtlAware = true,
91 inspectorInfo = {
92 name = "padding"
93 properties["horizontal"] = horizontal
94 properties["vertical"] = vertical
95 }
96 )
97
98 /**
99 * Apply [all] dp of additional space along each edge of the content, left, top, right and bottom.
100 * Padding is applied before content measurement and takes precedence; content may only be as large
101 * as the remaining space.
102 *
103 * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
104 * [Modifier.offset].
105 *
106 * Example usage:
107 *
108 * @sample androidx.compose.foundation.layout.samples.PaddingAllModifier
109 */
110 @Stable
paddingnull111 fun Modifier.padding(all: Dp) =
112 this then
113 PaddingElement(
114 start = all,
115 top = all,
116 end = all,
117 bottom = all,
118 rtlAware = true,
119 inspectorInfo = {
120 name = "padding"
121 value = all
122 }
123 )
124
125 /**
126 * Apply [PaddingValues] to the component as additional space along each edge of the content's left,
127 * top, right and bottom. Padding is applied before content measurement and takes precedence;
128 * content may only be as large as the remaining space.
129 *
130 * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
131 * [Modifier.offset].
132 *
133 * Example usage:
134 *
135 * @sample androidx.compose.foundation.layout.samples.PaddingValuesModifier
136 */
137 @Stable
paddingnull138 fun Modifier.padding(paddingValues: PaddingValues) =
139 this then
140 PaddingValuesElement(
141 paddingValues = paddingValues,
142 inspectorInfo = {
143 name = "padding"
144 properties["paddingValues"] = paddingValues
145 }
146 )
147
148 /**
149 * Apply additional space along each edge of the content in [Dp]: [left], [top], [right] and
150 * [bottom]. These paddings are applied without regard to the current [LayoutDirection], see
151 * [padding] to apply relative paddings. Padding is applied before content measurement and takes
152 * precedence; content may only be as large as the remaining space.
153 *
154 * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
155 * [Modifier.offset].
156 *
157 * Example usage:
158 *
159 * @sample androidx.compose.foundation.layout.samples.AbsolutePaddingModifier
160 */
161 @Stable
absolutePaddingnull162 fun Modifier.absolutePadding(left: Dp = 0.dp, top: Dp = 0.dp, right: Dp = 0.dp, bottom: Dp = 0.dp) =
163 this then
164 (PaddingElement(
165 start = left,
166 top = top,
167 end = right,
168 bottom = bottom,
169 rtlAware = false,
170 inspectorInfo = {
171 name = "absolutePadding"
172 properties["left"] = left
173 properties["top"] = top
174 properties["right"] = right
175 properties["bottom"] = bottom
176 }
177 ))
178
179 /**
180 * Describes a padding to be applied along the edges inside a box. See the [PaddingValues] factories
181 * and [Absolute] for convenient ways to build [PaddingValues].
182 */
183 @Stable
184 interface PaddingValues {
185 /** The padding to be applied along the left edge inside a box. */
calculateLeftPaddingnull186 fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp
187
188 /** The padding to be applied along the top edge inside a box. */
189 fun calculateTopPadding(): Dp
190
191 /** The padding to be applied along the right edge inside a box. */
192 fun calculateRightPadding(layoutDirection: LayoutDirection): Dp
193
194 /** The padding to be applied along the bottom edge inside a box. */
195 fun calculateBottomPadding(): Dp
196
197 /** Describes an absolute (RTL unaware) padding to be applied along the edges inside a box. */
198 @Immutable
199 class Absolute(
200 @Stable private val left: Dp = 0.dp,
201 @Stable private val top: Dp = 0.dp,
202 @Stable private val right: Dp = 0.dp,
203 @Stable private val bottom: Dp = 0.dp
204 ) : PaddingValues {
205
206 init {
207 requirePrecondition(
208 (left.value >= 0f) and
209 (top.value >= 0f) and
210 (right.value >= 0f) and
211 (bottom.value >= 0f)
212 ) {
213 "Padding must be non-negative"
214 }
215 }
216
217 override fun calculateLeftPadding(layoutDirection: LayoutDirection) = left
218
219 override fun calculateTopPadding() = top
220
221 override fun calculateRightPadding(layoutDirection: LayoutDirection) = right
222
223 override fun calculateBottomPadding() = bottom
224
225 override fun equals(other: Any?): Boolean {
226 if (other !is Absolute) return false
227 return left == other.left &&
228 top == other.top &&
229 right == other.right &&
230 bottom == other.bottom
231 }
232
233 override fun hashCode() =
234 ((left.hashCode() * 31 + top.hashCode()) * 31 + right.hashCode()) * 31 +
235 bottom.hashCode()
236
237 override fun toString() =
238 "PaddingValues.Absolute(left=$left, top=$top, right=$right, bottom=$bottom)"
239 }
240
241 companion object {
242 /** PaddingValues with all values `0.dp`. */
243 @Stable @get:Stable val Zero: PaddingValues = Absolute()
244 }
245 }
246
247 /**
248 * The padding to be applied along the start edge inside a box: along the left edge if the layout
249 * direction is LTR, or along the right edge for RTL.
250 */
251 @Stable
PaddingValuesnull252 fun PaddingValues.calculateStartPadding(layoutDirection: LayoutDirection) =
253 if (layoutDirection == LayoutDirection.Ltr) {
254 calculateLeftPadding(layoutDirection)
255 } else {
256 calculateRightPadding(layoutDirection)
257 }
258
259 /**
260 * The padding to be applied along the end edge inside a box: along the right edge if the layout
261 * direction is LTR, or along the left edge for RTL.
262 */
263 @Stable
PaddingValuesnull264 fun PaddingValues.calculateEndPadding(layoutDirection: LayoutDirection) =
265 if (layoutDirection == LayoutDirection.Ltr) {
266 calculateRightPadding(layoutDirection)
267 } else {
268 calculateLeftPadding(layoutDirection)
269 }
270
271 /** Creates a padding of [all] dp along all 4 edges. */
PaddingValuesnull272 @Stable fun PaddingValues(all: Dp): PaddingValues = PaddingValuesImpl(all, all, all, all)
273
274 /**
275 * Creates a padding of [horizontal] dp along the left and right edges, and of [vertical] dp along
276 * the top and bottom edges.
277 */
278 @Stable
279 fun PaddingValues(horizontal: Dp = 0.dp, vertical: Dp = 0.dp): PaddingValues =
280 PaddingValuesImpl(horizontal, vertical, horizontal, vertical)
281
282 /**
283 * Creates a padding to be applied along the edges inside a box. In LTR contexts [start] will be
284 * applied along the left edge and [end] will be applied along the right edge. In RTL contexts,
285 * [start] will correspond to the right edge and [end] to the left.
286 */
287 @Stable
288 fun PaddingValues(
289 start: Dp = 0.dp,
290 top: Dp = 0.dp,
291 end: Dp = 0.dp,
292 bottom: Dp = 0.dp
293 ): PaddingValues = PaddingValuesImpl(start, top, end, bottom)
294
295 @Immutable
296 internal class PaddingValuesImpl(
297 @Stable val start: Dp = 0.dp,
298 @Stable val top: Dp = 0.dp,
299 @Stable val end: Dp = 0.dp,
300 @Stable val bottom: Dp = 0.dp
301 ) : PaddingValues {
302
303 init {
304 requirePrecondition(
305 (start.value >= 0f) and (top.value >= 0f) and (end.value >= 0f) and (bottom.value >= 0f)
306 ) {
307 "Padding must be non-negative"
308 }
309 }
310
311 override fun calculateLeftPadding(layoutDirection: LayoutDirection) =
312 if (layoutDirection == LayoutDirection.Ltr) start else end
313
314 override fun calculateTopPadding() = top
315
316 override fun calculateRightPadding(layoutDirection: LayoutDirection) =
317 if (layoutDirection == LayoutDirection.Ltr) end else start
318
319 override fun calculateBottomPadding() = bottom
320
321 override fun equals(other: Any?): Boolean {
322 if (other !is PaddingValuesImpl) return false
323 return start == other.start &&
324 top == other.top &&
325 end == other.end &&
326 bottom == other.bottom
327 }
328
329 override fun hashCode() =
330 ((start.hashCode() * 31 + top.hashCode()) * 31 + end.hashCode()) * 31 + bottom.hashCode()
331
332 override fun toString() = "PaddingValues(start=$start, top=$top, end=$end, bottom=$bottom)"
333 }
334
335 private class PaddingElement(
336 var start: Dp = 0.dp,
337 var top: Dp = 0.dp,
338 var end: Dp = 0.dp,
339 var bottom: Dp = 0.dp,
340 var rtlAware: Boolean,
341 val inspectorInfo: InspectorInfo.() -> Unit
342 ) : ModifierNodeElement<PaddingNode>() {
343
344 init {
345 requirePrecondition(
346 (start.value >= 0f || start.isUnspecified) and
347 (top.value >= 0f || top.isUnspecified) and
348 (end.value >= 0f || end.isUnspecified) and
349 (bottom.value >= 0f || bottom.isUnspecified)
<lambda>null350 ) {
351 "Padding must be non-negative"
352 }
353 }
354
createnull355 override fun create(): PaddingNode {
356 return PaddingNode(start, top, end, bottom, rtlAware)
357 }
358
updatenull359 override fun update(node: PaddingNode) {
360 node.start = start
361 node.top = top
362 node.end = end
363 node.bottom = bottom
364 node.rtlAware = rtlAware
365 }
366
hashCodenull367 override fun hashCode(): Int {
368 var result = start.hashCode()
369 result = 31 * result + top.hashCode()
370 result = 31 * result + end.hashCode()
371 result = 31 * result + bottom.hashCode()
372 result = 31 * result + rtlAware.hashCode()
373 return result
374 }
375
equalsnull376 override fun equals(other: Any?): Boolean {
377 val otherModifierElement = other as? PaddingElement ?: return false
378 return start == otherModifierElement.start &&
379 top == otherModifierElement.top &&
380 end == otherModifierElement.end &&
381 bottom == otherModifierElement.bottom &&
382 rtlAware == otherModifierElement.rtlAware
383 }
384
inspectablePropertiesnull385 override fun InspectorInfo.inspectableProperties() {
386 inspectorInfo()
387 }
388 }
389
390 private class PaddingNode(
391 var start: Dp = 0.dp,
392 var top: Dp = 0.dp,
393 var end: Dp = 0.dp,
394 var bottom: Dp = 0.dp,
395 var rtlAware: Boolean
396 ) : LayoutModifierNode, Modifier.Node() {
397
measurenull398 override fun MeasureScope.measure(
399 measurable: Measurable,
400 constraints: Constraints
401 ): MeasureResult {
402
403 val horizontal = start.roundToPx() + end.roundToPx()
404 val vertical = top.roundToPx() + bottom.roundToPx()
405
406 val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
407
408 val width = constraints.constrainWidth(placeable.width + horizontal)
409 val height = constraints.constrainHeight(placeable.height + vertical)
410 return layout(width, height) {
411 if (rtlAware) {
412 placeable.placeRelative(start.roundToPx(), top.roundToPx())
413 } else {
414 placeable.place(start.roundToPx(), top.roundToPx())
415 }
416 }
417 }
418 }
419
420 private class PaddingValuesElement(
421 val paddingValues: PaddingValues,
422 val inspectorInfo: InspectorInfo.() -> Unit
423 ) : ModifierNodeElement<PaddingValuesModifier>() {
createnull424 override fun create(): PaddingValuesModifier {
425 return PaddingValuesModifier(paddingValues)
426 }
427
updatenull428 override fun update(node: PaddingValuesModifier) {
429 node.paddingValues = paddingValues
430 }
431
hashCodenull432 override fun hashCode(): Int = paddingValues.hashCode()
433
434 override fun equals(other: Any?): Boolean {
435 val otherElement = other as? PaddingValuesElement ?: return false
436 return paddingValues == otherElement.paddingValues
437 }
438
inspectablePropertiesnull439 override fun InspectorInfo.inspectableProperties() {
440 inspectorInfo()
441 }
442 }
443
444 private class PaddingValuesModifier(var paddingValues: PaddingValues) :
445 LayoutModifierNode, Modifier.Node() {
measurenull446 override fun MeasureScope.measure(
447 measurable: Measurable,
448 constraints: Constraints
449 ): MeasureResult {
450 val leftPadding = paddingValues.calculateLeftPadding(layoutDirection)
451 val topPadding = paddingValues.calculateTopPadding()
452 val rightPadding = paddingValues.calculateRightPadding(layoutDirection)
453 val bottomPadding = paddingValues.calculateBottomPadding()
454
455 requirePrecondition(
456 (leftPadding >= 0.dp) and
457 (topPadding >= 0.dp) and
458 (rightPadding >= 0.dp) and
459 (bottomPadding >= 0.dp)
460 ) {
461 "Padding must be non-negative"
462 }
463
464 val roundedLeftPadding = leftPadding.roundToPx()
465 val horizontal = roundedLeftPadding + rightPadding.roundToPx()
466
467 val roundedTopPadding = topPadding.roundToPx()
468 val vertical = roundedTopPadding + bottomPadding.roundToPx()
469
470 val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
471
472 val width = constraints.constrainWidth(placeable.width + horizontal)
473 val height = constraints.constrainHeight(placeable.height + vertical)
474 return layout(width, height) { placeable.place(roundedLeftPadding, roundedTopPadding) }
475 }
476 }
477