1 /*
<lambda>null2 * Copyright 2024 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.material3
18
19 import androidx.compose.foundation.gestures.awaitEachGesture
20 import androidx.compose.foundation.gestures.awaitFirstDown
21 import androidx.compose.foundation.gestures.waitForUpOrCancellation
22 import androidx.compose.foundation.hoverable
23 import androidx.compose.foundation.indication
24 import androidx.compose.foundation.interaction.Interaction
25 import androidx.compose.foundation.interaction.MutableInteractionSource
26 import androidx.compose.foundation.interaction.collectIsDraggedAsState
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.material3.tokens.DragHandleTokens
29 import androidx.compose.runtime.Composable
30 import androidx.compose.runtime.Immutable
31 import androidx.compose.runtime.getValue
32 import androidx.compose.runtime.mutableStateOf
33 import androidx.compose.runtime.remember
34 import androidx.compose.runtime.setValue
35 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.draw.drawBehind
37 import androidx.compose.ui.graphics.Color
38 import androidx.compose.ui.graphics.Shape
39 import androidx.compose.ui.graphics.graphicsLayer
40 import androidx.compose.ui.graphics.takeOrElse
41 import androidx.compose.ui.input.pointer.PointerEventPass
42 import androidx.compose.ui.input.pointer.pointerInput
43 import androidx.compose.ui.layout.layout
44 import androidx.compose.ui.unit.Constraints
45 import androidx.compose.ui.unit.DpSize
46 import androidx.compose.ui.unit.isSpecified
47 import androidx.compose.ui.util.fastRoundToInt
48
49 /**
50 * [Material Design drag
51 * handle](https://m3.material.io/foundations/layout/understanding-layout/parts-of-layout#314a4c32-be52-414c-8da7-31f059f1776d)
52 *
53 * A drag handle is a capsule-like shape that can be used by users to change component size and/or
54 * position by dragging. A typical usage of it will be pane expansion - when you split your screen
55 * into multiple panes, a drag handle is suggested to be used so users can drag it to change the
56 * proportion of how the screen is being split. Note that a vertically oriented drag handle is meant
57 * to convey horizontal drag motions.
58 *
59 * @sample androidx.compose.material3.samples.VerticalDragHandleSample
60 * @param modifier the [Modifier] to be applied to this drag handle.
61 * @param sizes sizes of this drag handle; see [VerticalDragHandleDefaults.sizes] for the default
62 * values.
63 * @param colors colors of this drag handle; see [VerticalDragHandleDefaults.colors] for the default
64 * values.
65 * @param shapes shapes of this drag handle; see [VerticalDragHandleDefaults.colors] for the default
66 * values.
67 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
68 * emitting [Interaction]s for this drag handle. You can use this to change the drag handle's
69 * appearance or preview the drag handle in different states. Note that if `null` is provided,
70 * interactions will still happen internally.
71 */
72 @Composable
73 fun VerticalDragHandle(
74 modifier: Modifier = Modifier,
75 sizes: DragHandleSizes = VerticalDragHandleDefaults.sizes(),
76 colors: DragHandleColors = VerticalDragHandleDefaults.colors(),
77 shapes: DragHandleShapes = VerticalDragHandleDefaults.shapes(),
78 interactionSource: MutableInteractionSource? = null,
79 ) {
80 @Suppress("NAME_SHADOWING")
81 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
82 val isDragged by interactionSource.collectIsDraggedAsState()
83 var isPressed by remember { mutableStateOf(false) }
84 Box(
85 modifier =
86 modifier
87 .minimumInteractiveComponentSize()
88 .hoverable(interactionSource)
89 .pressable(interactionSource, { isPressed = true }, { isPressed = false })
90 .graphicsLayer {
91 shape =
92 when {
93 isDragged -> shapes.draggedShape
94 isPressed -> shapes.pressedShape
95 else -> shapes.shape
96 }
97 clip = true
98 }
99 .layout { measurable, _ ->
100 val dragHandleSize =
101 when {
102 isDragged -> sizes.draggedSize
103 isPressed -> sizes.pressedSize
104 else -> sizes.size
105 }.toSize()
106 // set constraints here to be the size needed
107 val placeable =
108 measurable.measure(
109 Constraints.fixed(
110 dragHandleSize.width.fastRoundToInt(),
111 dragHandleSize.height.fastRoundToInt()
112 )
113 )
114 layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
115 }
116 .drawBehind {
117 drawRect(
118 when {
119 isDragged -> colors.draggedColor
120 isPressed -> colors.pressedColor
121 else -> colors.color
122 }
123 )
124 }
125 .indication(interactionSource, ripple())
126 )
127 }
128
129 /**
130 * Specifies the colors that will be used in a drag handle in different states.
131 *
132 * @param color the default color of the drag handle when it's not being pressed.
133 * @param pressedColor the color of the drag handle when it's being pressed but not dragged, by
134 * default it will be the same as [draggedColor].
135 * @param draggedColor the color of the drag handle when it's being dragged.
136 */
137 @Immutable
138 class DragHandleColors(val color: Color, val pressedColor: Color, val draggedColor: Color) {
equalsnull139 override fun equals(other: Any?): Boolean {
140 if (this === other) return true
141 if (other == null || other !is DragHandleColors) return false
142 if (color != other.color) return false
143 if (pressedColor != other.pressedColor) return false
144 if (draggedColor != other.draggedColor) return false
145 return true
146 }
147
hashCodenull148 override fun hashCode(): Int {
149 var result = color.hashCode()
150 result = 31 * result + pressedColor.hashCode()
151 result = 31 * result + draggedColor.hashCode()
152 return result
153 }
154 }
155
156 /**
157 * Specifies the shapes that will be used in a drag handle in different states.
158 *
159 * @param shape the default shape of the drag handle when it's not being pressed.
160 * @param pressedShape the shape of the drag handle when it's being pressed but not dragged, by
161 * default it will be the same as [draggedShape].
162 * @param draggedShape the shape of the drag handle when it's being dragged.
163 */
164 @Immutable
165 class DragHandleShapes(val shape: Shape, val pressedShape: Shape, val draggedShape: Shape) {
equalsnull166 override fun equals(other: Any?): Boolean {
167 if (this === other) return true
168 if (other == null || other !is DragHandleShapes) return false
169 if (shape != other.shape) return false
170 if (pressedShape != other.pressedShape) return false
171 if (draggedShape != other.draggedShape) return false
172 return true
173 }
174
hashCodenull175 override fun hashCode(): Int {
176 var result = shape.hashCode()
177 result = 31 * result + pressedShape.hashCode()
178 result = 31 * result + draggedShape.hashCode()
179 return result
180 }
181 }
182
183 /**
184 * Specifies the sizes that will be used in a drag handle in different states.
185 *
186 * @param size the default size of the drag handle when it's not being pressed.
187 * @param pressedSize the size of the drag handle when it's being pressed but not dragged, by
188 * default it will be the same as [draggedSize].
189 * @param draggedSize the size of the drag handle when it's being dragged.
190 */
191 @Immutable
192 class DragHandleSizes(val size: DpSize, val pressedSize: DpSize, val draggedSize: DpSize) {
equalsnull193 override fun equals(other: Any?): Boolean {
194 if (this === other) return true
195 if (other == null || other !is DragHandleSizes) return false
196 if (size != other.size) return false
197 if (pressedSize != other.pressedSize) return false
198 if (draggedSize != other.draggedSize) return false
199 return true
200 }
201
hashCodenull202 override fun hashCode(): Int {
203 var result = size.hashCode()
204 result = 31 * result + pressedSize.hashCode()
205 result = 31 * result + draggedSize.hashCode()
206 return result
207 }
208 }
209
210 /** Contains the baseline values used by a [VerticalDragHandle]. */
211 object VerticalDragHandleDefaults {
212 /**
213 * Creates a [DragHandleColors] that represents the default, pressed, and dragged colors used in
214 * a [VerticalDragHandle].
215 */
colorsnull216 @Composable fun colors(): DragHandleColors = MaterialTheme.colorScheme.colors
217
218 /**
219 * Creates a [DragHandleColors] that represents the default, pressed, and dragged colors used in
220 * a [VerticalDragHandle].
221 *
222 * @param color provides a different color to override the default color of the drag handle when
223 * it's not being pressed.
224 * @param pressedColor provides a different color to override the color of the drag handle when
225 * it's being pressed but not dragged.
226 * @param draggedColor provides a different color to override the color of the drag handle when
227 * it's being dragged.
228 */
229 @Composable
230 fun colors(
231 color: Color = Color.Unspecified,
232 pressedColor: Color = Color.Unspecified,
233 draggedColor: Color = Color.Unspecified
234 ): DragHandleColors =
235 with(MaterialTheme.colorScheme.colors) {
236 DragHandleColors(
237 color.takeOrElse { this.color },
238 pressedColor.takeOrElse { this.pressedColor },
239 draggedColor.takeOrElse { this.draggedColor },
240 )
241 }
242
243 /**
244 * Creates a [DragHandleShapes] that represents the default, pressed, and dragged shapes used in
245 * a [VerticalDragHandle].
246 */
shapesnull247 @Composable fun shapes(): DragHandleShapes = MaterialTheme.shapes.shapes
248
249 /**
250 * Creates a [DragHandleShapes] that represents the default, pressed, and dragged shapes used in
251 * a [VerticalDragHandle].
252 *
253 * @param shape provides a different shape to override the default shape of the drag handle when
254 * it's not being pressed.
255 * @param pressedShape provides a different shape to override the shape of the drag handle when
256 * it's being pressed but not dragged.
257 * @param draggedShape provides a different shape to override the shape of the drag handle when
258 * it's being dragged.
259 */
260 @Composable
261 fun shapes(
262 shape: Shape? = null,
263 pressedShape: Shape? = null,
264 draggedShape: Shape? = null
265 ): DragHandleShapes =
266 with(MaterialTheme.shapes.shapes) {
267 DragHandleShapes(
268 shape ?: this.shape,
269 pressedShape ?: this.pressedShape,
270 draggedShape ?: this.draggedShape,
271 )
272 }
273
274 /**
275 * Creates a [DragHandleSizes] that represents the default, pressed, and dragged sizes used in a
276 * [VerticalDragHandle].
277 */
sizesnull278 fun sizes(): DragHandleSizes = sizes
279
280 /**
281 * Creates a [DragHandleSizes] that represents the default, pressed, and dragged sizes used in a
282 * [VerticalDragHandle].
283 *
284 * @param size provides a different size to override the default size of the drag handle when
285 * it's not being pressed.
286 * @param pressedSize provides a different size to override the size of the drag handle when
287 * it's being pressed but not dragged.
288 * @param draggedSize provides a different size to override the size of the drag handle when
289 * it's being dragged.
290 */
291 fun sizes(
292 size: DpSize = DpSize.Unspecified,
293 pressedSize: DpSize = DpSize.Unspecified,
294 draggedSize: DpSize = DpSize.Unspecified
295 ): DragHandleSizes =
296 with(sizes) {
297 DragHandleSizes(
298 if (size.isSpecified) size else this.size,
299 if (pressedSize.isSpecified) pressedSize else this.pressedSize,
300 if (draggedSize.isSpecified) draggedSize else this.draggedSize,
301 )
302 }
303
304 private val ColorScheme.colors: DragHandleColors
305 get() {
306 return defaultVerticalDragHandleColorsCached
307 ?: DragHandleColors(
308 color = fromToken(DragHandleTokens.Color),
309 pressedColor = fromToken(DragHandleTokens.PressedColor),
310 draggedColor = fromToken(DragHandleTokens.DraggedColor),
311 )
<lambda>null312 .also { defaultVerticalDragHandleColorsCached = it }
313 }
314
315 private val Shapes.shapes: DragHandleShapes
316 get() {
317 return defaultVerticalDragHandleShapesCached
318 ?: DragHandleShapes(
319 shape = fromToken(DragHandleTokens.Shape),
320 pressedShape = fromToken(DragHandleTokens.PressedShape),
321 draggedShape = fromToken(DragHandleTokens.DraggedShape),
322 )
<lambda>null323 .also { defaultVerticalDragHandleShapesCached = it }
324 }
325
326 private val sizes =
327 DragHandleSizes(
328 size = DpSize(DragHandleTokens.Width, DragHandleTokens.Height),
329 pressedSize = DpSize(DragHandleTokens.PressedWidth, DragHandleTokens.PressedHeight),
330 draggedSize = DpSize(DragHandleTokens.DraggedWidth, DragHandleTokens.DraggedHeight)
331 )
332 }
333
pressablenull334 private fun Modifier.pressable(
335 interactionSource: MutableInteractionSource,
336 onPressed: () -> Unit,
337 onReleasedOrCancelled: () -> Unit
338 ): Modifier =
339 pointerInput(interactionSource) {
340 awaitEachGesture {
341 awaitFirstDown(pass = PointerEventPass.Initial)
342 onPressed()
343 waitForUpOrCancellation(pass = PointerEventPass.Initial)
344 onReleasedOrCancelled()
345 }
346 }
347