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.ui
18
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.runtime.Stable
21 import androidx.compose.ui.unit.IntOffset
22 import androidx.compose.ui.unit.IntSize
23 import androidx.compose.ui.unit.LayoutDirection
24 import androidx.compose.ui.util.fastRoundToInt
25
26 /**
27 * An interface to calculate the position of a sized box inside an available space. [Alignment] is
28 * often used to define the alignment of a layout inside a parent layout.
29 *
30 * @see AbsoluteAlignment
31 * @see BiasAlignment
32 * @see BiasAbsoluteAlignment
33 */
34 @Stable
interfacenull35 fun interface Alignment {
36 /**
37 * Calculates the position of a box of size [size] relative to the top left corner of an area of
38 * size [space]. The returned offset can be negative or larger than `space - size`, meaning that
39 * the box will be positioned partially or completely outside the area.
40 */
41 fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset
42
43 /**
44 * An interface to calculate the position of box of a certain width inside an available width.
45 * [Alignment.Horizontal] is often used to define the horizontal alignment of a layout inside a
46 * parent layout.
47 */
48 @Stable
49 fun interface Horizontal {
50 /**
51 * Calculates the horizontal position of a box of width [size] relative to the left side of
52 * an area of width [space]. The returned offset can be negative or larger than `space -
53 * size` meaning that the box will be positioned partially or completely outside the area.
54 */
55 fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int
56
57 /**
58 * Combine this instance's horizontal alignment with [other]'s vertical alignment to create
59 * an [Alignment].
60 */
61 operator fun plus(other: Vertical): Alignment = CombinedAlignment(this, other)
62 }
63
64 /**
65 * An interface to calculate the position of a box of a certain height inside an available
66 * height. [Alignment.Vertical] is often used to define the vertical alignment of a layout
67 * inside a parent layout.
68 */
69 @Stable
70 fun interface Vertical {
71 /**
72 * Calculates the vertical position of a box of height [size] relative to the top edge of an
73 * area of height [space]. The returned offset can be negative or larger than `space - size`
74 * meaning that the box will be positioned partially or completely outside the area.
75 */
76 fun align(size: Int, space: Int): Int
77
78 /**
79 * Combine this instance's vertical alignment with [other]'s horizontal alignment to create
80 * an [Alignment].
81 */
82 operator fun plus(other: Horizontal): Alignment = CombinedAlignment(other, this)
83 }
84
85 /** A collection of common [Alignment]s aware of layout direction. */
86 companion object {
87 // 2D Alignments.
88 @Stable val TopStart: Alignment = BiasAlignment(-1f, -1f)
89 @Stable val TopCenter: Alignment = BiasAlignment(0f, -1f)
90 @Stable val TopEnd: Alignment = BiasAlignment(1f, -1f)
91 @Stable val CenterStart: Alignment = BiasAlignment(-1f, 0f)
92 @Stable val Center: Alignment = BiasAlignment(0f, 0f)
93 @Stable val CenterEnd: Alignment = BiasAlignment(1f, 0f)
94 @Stable val BottomStart: Alignment = BiasAlignment(-1f, 1f)
95 @Stable val BottomCenter: Alignment = BiasAlignment(0f, 1f)
96 @Stable val BottomEnd: Alignment = BiasAlignment(1f, 1f)
97
98 // 1D Alignment.Verticals.
99 @Stable val Top: Vertical = BiasAlignment.Vertical(-1f)
100 @Stable val CenterVertically: Vertical = BiasAlignment.Vertical(0f)
101 @Stable val Bottom: Vertical = BiasAlignment.Vertical(1f)
102
103 // 1D Alignment.Horizontals.
104 @Stable val Start: Horizontal = BiasAlignment.Horizontal(-1f)
105 @Stable val CenterHorizontally: Horizontal = BiasAlignment.Horizontal(0f)
106 @Stable val End: Horizontal = BiasAlignment.Horizontal(1f)
107 }
108 }
109
110 private class CombinedAlignment(
111 private val horizontal: Alignment.Horizontal,
112 private val vertical: Alignment.Vertical,
113 ) : Alignment {
alignnull114 override fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset {
115 val x = horizontal.align(size.width, space.width, layoutDirection)
116 val y = vertical.align(size.height, space.height)
117 return IntOffset(x, y)
118 }
119 }
120
121 /** A collection of common [Alignment]s unaware of the layout direction. */
122 object AbsoluteAlignment {
123 // 2D AbsoluteAlignments.
124 @Stable val TopLeft: Alignment = BiasAbsoluteAlignment(-1f, -1f)
125 @Stable val TopRight: Alignment = BiasAbsoluteAlignment(1f, -1f)
126 @Stable val CenterLeft: Alignment = BiasAbsoluteAlignment(-1f, 0f)
127 @Stable val CenterRight: Alignment = BiasAbsoluteAlignment(1f, 0f)
128 @Stable val BottomLeft: Alignment = BiasAbsoluteAlignment(-1f, 1f)
129 @Stable val BottomRight: Alignment = BiasAbsoluteAlignment(1f, 1f)
130
131 // 1D BiasAbsoluteAlignment.Horizontals.
132 @Stable val Left: Alignment.Horizontal = BiasAbsoluteAlignment.Horizontal(-1f)
133 @Stable val Right: Alignment.Horizontal = BiasAbsoluteAlignment.Horizontal(1f)
134 }
135
136 /**
137 * An [Alignment] specified by bias: for example, a bias of -1 represents alignment to the
138 * start/top, a bias of 0 will represent centering, and a bias of 1 will represent end/bottom. Any
139 * value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained alignment
140 * will position the aligned size fully inside the available space, while outside the range it will
141 * the aligned size will be positioned partially or completely outside.
142 *
143 * @see BiasAbsoluteAlignment
144 * @see Alignment
145 */
146 @Immutable
147 @Suppress("DataClassDefinition")
148 data class BiasAlignment(val horizontalBias: Float, val verticalBias: Float) : Alignment {
alignnull149 override fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset {
150 // Convert to Px first and only round at the end, to avoid rounding twice while calculating
151 // the new positions
152 val centerX = (space.width - size.width).toFloat() / 2f
153 val centerY = (space.height - size.height).toFloat() / 2f
154 val resolvedHorizontalBias =
155 if (layoutDirection == LayoutDirection.Ltr) {
156 horizontalBias
157 } else {
158 -1 * horizontalBias
159 }
160
161 val x = centerX * (1 + resolvedHorizontalBias)
162 val y = centerY * (1 + verticalBias)
163 return IntOffset(x.fastRoundToInt(), y.fastRoundToInt())
164 }
165
166 /**
167 * An [Alignment.Horizontal] specified by bias: for example, a bias of -1 represents alignment
168 * to the start, a bias of 0 will represent centering, and a bias of 1 will represent end. Any
169 * value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained
170 * alignment will position the aligned size fully inside the available space, while outside the
171 * range it will the aligned size will be positioned partially or completely outside.
172 *
173 * @see BiasAbsoluteAlignment.Horizontal
174 * @see Vertical
175 */
176 @Immutable
177 @Suppress("DataClassDefinition")
178 data class Horizontal(val bias: Float) : Alignment.Horizontal {
alignnull179 override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int {
180 // Convert to Px first and only round at the end, to avoid rounding twice while
181 // calculating the new positions
182 val center = (space - size).toFloat() / 2f
183 val resolvedBias = if (layoutDirection == LayoutDirection.Ltr) bias else -1 * bias
184 return (center * (1 + resolvedBias)).fastRoundToInt()
185 }
186
plusnull187 override fun plus(other: Alignment.Vertical): Alignment {
188 return when (other) {
189 is Vertical -> BiasAlignment(bias, other.bias)
190 else -> super.plus(other)
191 }
192 }
193 }
194
195 /**
196 * An [Alignment.Vertical] specified by bias: for example, a bias of -1 represents alignment to
197 * the top, a bias of 0 will represent centering, and a bias of 1 will represent bottom. Any
198 * value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained
199 * alignment will position the aligned size fully inside the available space, while outside the
200 * range it will the aligned size will be positioned partially or completely outside.
201 *
202 * @see Horizontal
203 */
204 @Immutable
205 @Suppress("DataClassDefinition")
206 data class Vertical(val bias: Float) : Alignment.Vertical {
alignnull207 override fun align(size: Int, space: Int): Int {
208 // Convert to Px first and only round at the end, to avoid rounding twice while
209 // calculating the new positions
210 val center = (space - size).toFloat() / 2f
211 return (center * (1 + bias)).fastRoundToInt()
212 }
213
plusnull214 override fun plus(other: Alignment.Horizontal): Alignment {
215 return when (other) {
216 is Horizontal -> BiasAlignment(other.bias, bias)
217 is BiasAbsoluteAlignment.Horizontal -> BiasAbsoluteAlignment(other.bias, bias)
218 else -> super.plus(other)
219 }
220 }
221 }
222 }
223
224 /**
225 * An [Alignment] specified by bias: for example, a bias of -1 represents alignment to the left/top,
226 * a bias of 0 will represent centering, and a bias of 1 will represent right/bottom. Any value can
227 * be specified to obtain an alignment. Inside the [-1, 1] range, the obtained alignment will
228 * position the aligned size fully inside the available space, while outside the range it will the
229 * aligned size will be positioned partially or completely outside.
230 *
231 * @see AbsoluteAlignment
232 * @see Alignment
233 */
234 @Immutable
235 @Suppress("DataClassDefinition")
236 data class BiasAbsoluteAlignment(val horizontalBias: Float, val verticalBias: Float) : Alignment {
237 /**
238 * Returns the position of a 2D point in a container of a given size, according to this
239 * [BiasAbsoluteAlignment]. The position will not be mirrored in Rtl context.
240 */
alignnull241 override fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset {
242 // Convert to Px first and only round at the end, to avoid rounding twice while calculating
243 // the new positions
244 val remaining = IntSize(space.width - size.width, space.height - size.height)
245 val centerX = remaining.width.toFloat() / 2f
246 val centerY = remaining.height.toFloat() / 2f
247
248 val x = centerX * (1 + horizontalBias)
249 val y = centerY * (1 + verticalBias)
250 return IntOffset(x.fastRoundToInt(), y.fastRoundToInt())
251 }
252
253 /**
254 * An [Alignment.Horizontal] specified by bias: for example, a bias of -1 represents alignment
255 * to the left, a bias of 0 will represent centering, and a bias of 1 will represent right. Any
256 * value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained
257 * alignment will position the aligned size fully inside the available space, while outside the
258 * range it will the aligned size will be positioned partially or completely outside.
259 *
260 * @see BiasAlignment.Horizontal
261 */
262 @Immutable
263 @Suppress("DataClassDefinition")
264 data class Horizontal(val bias: Float) : Alignment.Horizontal {
265 /**
266 * Returns the position of a 2D point in a container of a given size, according to this
267 * [BiasAbsoluteAlignment.Horizontal]. This position will not be mirrored in Rtl context.
268 */
alignnull269 override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int {
270 // Convert to Px first and only round at the end, to avoid rounding twice while
271 // calculating the new positions
272 val center = (space - size).toFloat() / 2f
273 return (center * (1 + bias)).fastRoundToInt()
274 }
275
plusnull276 override fun plus(other: Alignment.Vertical): Alignment {
277 return when (other) {
278 is BiasAlignment.Vertical -> BiasAbsoluteAlignment(bias, other.bias)
279 else -> super.plus(other)
280 }
281 }
282 }
283 }
284