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