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