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