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.geometry.Offset
20 import androidx.compose.ui.geometry.Rect
21 import androidx.compose.ui.graphics.Matrix
22 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
23 import androidx.compose.ui.internal.throwUnsupportedOperationException
24 import androidx.compose.ui.node.NodeCoordinator
25 import androidx.compose.ui.unit.IntSize
26 import androidx.compose.ui.util.fastCoerceIn
27 import androidx.compose.ui.util.fastMaxOf
28 import androidx.compose.ui.util.fastMinOf
29
30 /** A holder of the measured bounds for the [Layout]. */
31 @JvmDefaultWithCompatibility
32 interface LayoutCoordinates {
33 /** The size of this layout in the local coordinates space. */
34 val size: IntSize
35
36 /** The alignment lines provided for this layout, not including inherited lines. */
37 val providedAlignmentLines: Set<AlignmentLine>
38
39 /** The coordinates of the parent layout. Null if there is no parent. */
40 val parentLayoutCoordinates: LayoutCoordinates?
41
42 /**
43 * The coordinates of the parent layout modifier or parent layout if there is no parent layout
44 * modifier, or `null` if there is no parent.
45 */
46 val parentCoordinates: LayoutCoordinates?
47
48 /** Returns false if the corresponding layout was detached from the hierarchy. */
49 val isAttached: Boolean
50
51 /**
52 * Indicates whether the corresponding Layout is expected to change its [Offset] in small
53 * increments (such as when its parent is a `Scroll`).
54 *
55 * In those situations, the corresponding placed [LayoutCoordinates] will have their
56 * [introducesMotionFrameOfReference] return `true`.
57 *
58 * Custom Layouts that are expected to have similar behaviors should place their children using
59 * [Placeable.PlacementScope.withMotionFrameOfReferencePlacement].
60 *
61 * You may then use [localPositionOf] with `includeMotionFrameOfReference = false` to query a
62 * Layout's position such that it excludes all [Offset] introduced by those Layouts.
63 *
64 * This is typically helpful when deciding when to animate an [approachLayout] using
65 * [LookaheadScope] coordinates. As you probably don't want to trigger animations on small
66 * positional increments.
67 *
68 * @see Placeable.PlacementScope.withMotionFrameOfReferencePlacement
69 * @see localPositionOf
70 */
71 @Suppress("GetterSetterNames") // Preferred name
72 val introducesMotionFrameOfReference: Boolean
73 get() = false
74
75 /**
76 * Converts [relativeToScreen] relative to the device's screen's origin into an [Offset]
77 * relative to this layout. Returns [Offset.Unspecified] if the conversion cannot be performed.
78 */
screenToLocalnull79 fun screenToLocal(relativeToScreen: Offset): Offset = Offset.Unspecified
80
81 /**
82 * Converts [relativeToLocal] position within this layout into an [Offset] relative to the
83 * device's screen. Returns [Offset.Unspecified] if the conversion cannot be performed.
84 */
85 fun localToScreen(relativeToLocal: Offset): Offset = Offset.Unspecified
86
87 /**
88 * Converts [relativeToWindow] relative to the window's origin into an [Offset] relative to this
89 * layout.
90 */
91 fun windowToLocal(relativeToWindow: Offset): Offset
92
93 /**
94 * Converts [relativeToLocal] position within this layout into an [Offset] relative to the
95 * window's origin.
96 */
97 fun localToWindow(relativeToLocal: Offset): Offset
98
99 /** Converts a local position within this layout into an offset from the root composable. */
100 fun localToRoot(relativeToLocal: Offset): Offset
101
102 /**
103 * Converts an [relativeToSource] in [sourceCoordinates] space into local coordinates.
104 * [sourceCoordinates] may be any [LayoutCoordinates] that belong to the same compose layout
105 * hierarchy.
106 *
107 * By default, includes the [Offset] when [introducesMotionFrameOfReference] is `true`. But you
108 * may exclude it from the calculation by using the overload that takes
109 * `includeMotionFrameOfReference` and passing it as `false`.
110 */
111 fun localPositionOf(sourceCoordinates: LayoutCoordinates, relativeToSource: Offset): Offset
112
113 /**
114 * Converts an [relativeToSource] in [sourceCoordinates] space into local coordinates.
115 * [sourceCoordinates] may be any [LayoutCoordinates] that belong to the same compose layout
116 * hierarchy.
117 *
118 * Use [includeMotionFrameOfReference] to decide whether to include the [Offset] of any
119 * `LayoutCoordinate` that returns `true` in the [includeMotionFrameOfReference] flag.
120 *
121 * In other words, passing [includeMotionFrameOfReference] as `false`, returns a calculation
122 * that excludes the [Offset] set from Layouts that place their children using
123 * [Placeable.PlacementScope.withMotionFrameOfReferencePlacement].
124 */
125 fun localPositionOf(
126 sourceCoordinates: LayoutCoordinates,
127 relativeToSource: Offset = Offset.Zero,
128 includeMotionFrameOfReference: Boolean = true
129 ): Offset {
130 throw UnsupportedOperationException(
131 "localPositionOf is not implemented on this LayoutCoordinates"
132 )
133 }
134
135 /**
136 * Returns the bounding box of [sourceCoordinates] in the local coordinates. If [clipBounds] is
137 * `true`, any clipping that occurs between [sourceCoordinates] and this layout will affect the
138 * returned bounds, and can even result in an empty rectangle if clipped regions do not overlap.
139 * If [clipBounds] is false, the bounding box of [sourceCoordinates] will be converted to local
140 * coordinates irrespective of any clipping applied between the layouts.
141 *
142 * When rotation or scaling is applied, the bounding box of the rotated or scaled value will be
143 * computed in the local coordinates. For example, if a 40 pixels x 20 pixel layout is rotated
144 * 90 degrees, the bounding box will be 20 pixels x 40 pixels in its parent's coordinates.
145 */
localBoundingBoxOfnull146 fun localBoundingBoxOf(sourceCoordinates: LayoutCoordinates, clipBounds: Boolean = true): Rect
147
148 /**
149 * Modifies [matrix] to be a transform to convert a coordinate in [sourceCoordinates] to a
150 * coordinate in `this` [LayoutCoordinates].
151 */
152 @Suppress("DocumentExceptions")
153 fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
154 throwUnsupportedOperationException(
155 "transformFrom is not implemented on this LayoutCoordinates"
156 )
157 }
158
159 /**
160 * Takes a [matrix] which transforms some coordinate system `C` to local coordinates, and
161 * updates the matrix to transform from `C` to screen coordinates instead.
162 */
163 @Suppress("DocumentExceptions")
transformToScreennull164 fun transformToScreen(matrix: Matrix) {
165 throw UnsupportedOperationException(
166 "transformToScreen is not implemented on this LayoutCoordinates"
167 )
168 }
169
170 /**
171 * Returns the position in pixels of an [alignment line][AlignmentLine], or
172 * [AlignmentLine.Unspecified] if the line is not provided.
173 */
getnull174 operator fun get(alignmentLine: AlignmentLine): Int
175 }
176
177 /** The position of this layout inside the root composable. */
178 fun LayoutCoordinates.positionInRoot(): Offset = localToRoot(Offset.Zero)
179
180 /** The position of this layout relative to the window. */
181 fun LayoutCoordinates.positionInWindow(): Offset = localToWindow(Offset.Zero)
182
183 /**
184 * The position of this layout on the device's screen. Returns [Offset.Unspecified] if the
185 * conversion cannot be performed.
186 */
187 fun LayoutCoordinates.positionOnScreen(): Offset = localToScreen(Offset.Zero)
188
189 /** The boundaries of this layout inside the root composable. */
190 fun LayoutCoordinates.boundsInRoot(): Rect = findRootCoordinates().localBoundingBoxOf(this)
191
192 /** The boundaries of this layout relative to the window's origin. */
193 fun LayoutCoordinates.boundsInWindow(): Rect {
194 val root = findRootCoordinates()
195 val rootWidth = root.size.width.toFloat()
196 val rootHeight = root.size.height.toFloat()
197
198 val bounds = root.localBoundingBoxOf(this)
199 val boundsLeft = bounds.left.fastCoerceIn(0f, rootWidth)
200 val boundsTop = bounds.top.fastCoerceIn(0f, rootHeight)
201 val boundsRight = bounds.right.fastCoerceIn(0f, rootWidth)
202 val boundsBottom = bounds.bottom.fastCoerceIn(0f, rootHeight)
203
204 if (boundsLeft == boundsRight || boundsTop == boundsBottom) {
205 return Rect.Zero
206 }
207
208 val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
209 val topRight = root.localToWindow(Offset(boundsRight, boundsTop))
210 val bottomRight = root.localToWindow(Offset(boundsRight, boundsBottom))
211 val bottomLeft = root.localToWindow(Offset(boundsLeft, boundsBottom))
212
213 val topLeftX = topLeft.x
214 val topRightX = topRight.x
215 val bottomLeftX = bottomLeft.x
216 val bottomRightX = bottomRight.x
217
218 val left = fastMinOf(topLeftX, topRightX, bottomLeftX, bottomRightX)
219 val right = fastMaxOf(topLeftX, topRightX, bottomLeftX, bottomRightX)
220
221 val topLeftY = topLeft.y
222 val topRightY = topRight.y
223 val bottomLeftY = bottomLeft.y
224 val bottomRightY = bottomRight.y
225
226 val top = fastMinOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
227 val bottom = fastMaxOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
228
229 return Rect(left, top, right, bottom)
230 }
231
232 /** Returns the position of the top-left in the parent's content area or (0, 0) for the root. */
LayoutCoordinatesnull233 fun LayoutCoordinates.positionInParent(): Offset =
234 parentLayoutCoordinates?.localPositionOf(this, Offset.Zero) ?: Offset.Zero
235
236 /**
237 * Returns the bounding box of the child in the parent's content area, including any clipping done
238 * with respect to the parent. For the root, the bounds is positioned at (0, 0) and sized to the
239 * size of the root.
240 */
241 fun LayoutCoordinates.boundsInParent(): Rect =
242 parentLayoutCoordinates?.localBoundingBoxOf(this)
243 ?: Rect(0f, 0f, size.width.toFloat(), size.height.toFloat())
244
245 /**
246 * Walks up the [LayoutCoordinates] hierarchy to find the [LayoutCoordinates] whose
247 * [LayoutCoordinates.parentCoordinates] is `null` and returns it. If
248 * [LayoutCoordinates.isAttached], this will have the size of the
249 * [ComposeView][androidx.compose.ui.platform.ComposeView].
250 */
251 fun LayoutCoordinates.findRootCoordinates(): LayoutCoordinates {
252 var root = this
253 var parent = root.parentLayoutCoordinates
254 while (parent != null) {
255 root = parent
256 parent = root.parentLayoutCoordinates
257 }
258 var rootCoordinator = root as? NodeCoordinator ?: return root
259 var parentCoordinator = rootCoordinator.wrappedBy
260 while (parentCoordinator != null) {
261 rootCoordinator = parentCoordinator
262 parentCoordinator = parentCoordinator.wrappedBy
263 }
264 return rootCoordinator
265 }
266