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