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.foundation.layout
18 
19 import androidx.compose.runtime.Stable
20 import androidx.compose.ui.Modifier
21 import androidx.compose.ui.layout.Measurable
22 import androidx.compose.ui.layout.MeasureResult
23 import androidx.compose.ui.layout.MeasureScope
24 import androidx.compose.ui.node.LayoutModifierNode
25 import androidx.compose.ui.node.ModifierNodeElement
26 import androidx.compose.ui.node.invalidatePlacement
27 import androidx.compose.ui.platform.InspectorInfo
28 import androidx.compose.ui.unit.Constraints
29 import androidx.compose.ui.unit.Density
30 import androidx.compose.ui.unit.Dp
31 import androidx.compose.ui.unit.IntOffset
32 import androidx.compose.ui.unit.dp
33 
34 /**
35  * Offset the content by ([x] dp, [y] dp). The offsets can be positive as well as non-positive.
36  * Applying an offset only changes the position of the content, without interfering with its size
37  * measurement.
38  *
39  * This modifier will automatically adjust the horizontal offset according to the layout direction:
40  * when the layout direction is LTR, positive [x] offsets will move the content to the right and
41  * when the layout direction is RTL, positive [x] offsets will move the content to the left. For a
42  * modifier that offsets without considering layout direction, see [absoluteOffset].
43  *
44  * Example usage:
45  *
46  * @sample androidx.compose.foundation.layout.samples.OffsetModifier
47  * @see absoluteOffset
48  */
49 @Stable
offsetnull50 fun Modifier.offset(x: Dp = 0.dp, y: Dp = 0.dp) =
51     this then
52         OffsetElement(
53             x = x,
54             y = y,
55             rtlAware = true,
56             inspectorInfo = {
57                 name = "offset"
58                 properties["x"] = x
59                 properties["y"] = y
60             }
61         )
62 
63 /**
64  * Offset the content by ([x] dp, [y] dp). The offsets can be positive as well as non-positive.
65  * Applying an offset only changes the position of the content, without interfering with its size
66  * measurement.
67  *
68  * This modifier will not consider layout direction when calculating the position of the content: a
69  * positive [x] offset will always move the content to the right. For a modifier that considers the
70  * layout direction when applying the offset, see [offset].
71  *
72  * Example usage:
73  *
74  * @sample androidx.compose.foundation.layout.samples.AbsoluteOffsetModifier
75  * @see offset
76  */
77 @Stable
Modifiernull78 fun Modifier.absoluteOffset(x: Dp = 0.dp, y: Dp = 0.dp) =
79     this then
80         OffsetElement(
81             x = x,
82             y = y,
83             rtlAware = false,
84             inspectorInfo = {
85                 name = "absoluteOffset"
86                 properties["x"] = x
87                 properties["y"] = y
88             }
89         )
90 
91 /**
92  * Offset the content by [offset] px. The offsets can be positive as well as non-positive. Applying
93  * an offset only changes the position of the content, without interfering with its size
94  * measurement.
95  *
96  * This modifier is designed to be used for offsets that change, possibly due to user interactions.
97  * It avoids recomposition when the offset is changing, and also adds a graphics layer that prevents
98  * unnecessary redrawing of the context when the offset is changing.
99  *
100  * This modifier will automatically adjust the horizontal offset according to the layout direction:
101  * when the LD is LTR, positive horizontal offsets will move the content to the right and when the
102  * LD is RTL, positive horizontal offsets will move the content to the left. For a modifier that
103  * offsets without considering layout direction, see [absoluteOffset].
104  *
105  * Example usage:
106  *
107  * @sample androidx.compose.foundation.layout.samples.OffsetPxModifier
108  * @see [absoluteOffset]
109  */
offsetnull110 fun Modifier.offset(offset: Density.() -> IntOffset) =
111     this then
112         OffsetPxElement(
113             offset = offset,
114             rtlAware = true,
115             inspectorInfo = {
116                 name = "offset"
117                 properties["offset"] = offset
118             }
119         )
120 
121 /**
122  * Offset the content by [offset] px. The offsets can be positive as well as non-positive. Applying
123  * an offset only changes the position of the content, without interfering with its size
124  * measurement.
125  *
126  * This modifier is designed to be used for offsets that change, possibly due to user interactions.
127  * It avoids recomposition when the offset is changing, and also adds a graphics layer that prevents
128  * unnecessary redrawing of the context when the offset is changing.
129  *
130  * This modifier will not consider layout direction when calculating the position of the content: a
131  * positive horizontal offset will always move the content to the right. For a modifier that
132  * considers layout direction when applying the offset, see [offset].
133  *
134  * Example usage:
135  *
136  * @sample androidx.compose.foundation.layout.samples.AbsoluteOffsetPxModifier
137  * @see offset
138  */
Modifiernull139 fun Modifier.absoluteOffset(offset: Density.() -> IntOffset) =
140     this then
141         OffsetPxElement(
142             offset = offset,
143             rtlAware = false,
144             inspectorInfo = {
145                 name = "absoluteOffset"
146                 properties["offset"] = offset
147             }
148         )
149 
150 private class OffsetElement(
151     val x: Dp,
152     val y: Dp,
153     val rtlAware: Boolean,
154     val inspectorInfo: InspectorInfo.() -> Unit
155 ) : ModifierNodeElement<OffsetNode>() {
createnull156     override fun create(): OffsetNode {
157         return OffsetNode(x, y, rtlAware)
158     }
159 
updatenull160     override fun update(node: OffsetNode) {
161         node.update(x, y, rtlAware)
162     }
163 
equalsnull164     override fun equals(other: Any?): Boolean {
165         if (this === other) return true
166         val otherModifierElement = other as? OffsetElement ?: return false
167 
168         return x == otherModifierElement.x &&
169             y == otherModifierElement.y &&
170             rtlAware == otherModifierElement.rtlAware
171     }
172 
hashCodenull173     override fun hashCode(): Int {
174         var result = x.hashCode()
175         result = 31 * result + y.hashCode()
176         result = 31 * result + rtlAware.hashCode()
177         return result
178     }
179 
toStringnull180     override fun toString(): String = "OffsetModifierElement(x=$x, y=$y, rtlAware=$rtlAware)"
181 
182     override fun InspectorInfo.inspectableProperties() {
183         inspectorInfo()
184     }
185 }
186 
187 private class OffsetNode(var x: Dp, var y: Dp, var rtlAware: Boolean) :
188     LayoutModifierNode, Modifier.Node() {
189 
190     override val shouldAutoInvalidate: Boolean = false
191 
updatenull192     fun update(x: Dp, y: Dp, rtlAware: Boolean) {
193         if (this.x != x || this.y != y || this.rtlAware != rtlAware) invalidatePlacement()
194         this.x = x
195         this.y = y
196         this.rtlAware = rtlAware
197     }
198 
measurenull199     override fun MeasureScope.measure(
200         measurable: Measurable,
201         constraints: Constraints
202     ): MeasureResult {
203         val placeable = measurable.measure(constraints)
204         return layout(placeable.width, placeable.height) {
205             if (rtlAware) {
206                 placeable.placeRelative(x.roundToPx(), y.roundToPx())
207             } else {
208                 placeable.place(x.roundToPx(), y.roundToPx())
209             }
210         }
211     }
212 }
213 
214 private class OffsetPxElement(
215     val offset: Density.() -> IntOffset,
216     val rtlAware: Boolean,
217     val inspectorInfo: InspectorInfo.() -> Unit
218 ) : ModifierNodeElement<OffsetPxNode>() {
createnull219     override fun create(): OffsetPxNode {
220         return OffsetPxNode(offset, rtlAware)
221     }
222 
updatenull223     override fun update(node: OffsetPxNode) {
224         node.update(offset, rtlAware)
225     }
226 
equalsnull227     override fun equals(other: Any?): Boolean {
228         if (this === other) return true
229         val otherModifier = other as? OffsetPxElement ?: return false
230 
231         return offset === otherModifier.offset && rtlAware == otherModifier.rtlAware
232     }
233 
toStringnull234     override fun toString(): String = "OffsetPxModifier(offset=$offset, rtlAware=$rtlAware)"
235 
236     override fun hashCode(): Int {
237         var result = offset.hashCode()
238         result = 31 * result + rtlAware.hashCode()
239         return result
240     }
241 
inspectablePropertiesnull242     override fun InspectorInfo.inspectableProperties() {
243         inspectorInfo()
244     }
245 }
246 
247 private class OffsetPxNode(var offset: Density.() -> IntOffset, var rtlAware: Boolean) :
248     LayoutModifierNode, Modifier.Node() {
249 
250     override val shouldAutoInvalidate: Boolean = false
251 
updatenull252     fun update(offset: Density.() -> IntOffset, rtlAware: Boolean) {
253         if (this.offset !== offset || this.rtlAware != rtlAware) invalidatePlacement()
254         this.offset = offset
255         this.rtlAware = rtlAware
256     }
257 
measurenull258     override fun MeasureScope.measure(
259         measurable: Measurable,
260         constraints: Constraints
261     ): MeasureResult {
262         val placeable = measurable.measure(constraints)
263         return layout(placeable.width, placeable.height) {
264             val offsetValue = offset()
265             if (rtlAware) {
266                 placeable.placeRelativeWithLayer(offsetValue.x, offsetValue.y)
267             } else {
268                 placeable.placeWithLayer(offsetValue.x, offsetValue.y)
269             }
270         }
271     }
272 }
273