@@ -6,11 +6,19 @@ import androidx.compose.animation.core.animateDpAsState
66import androidx.compose.animation.core.spring
77import androidx.compose.animation.core.tween
88import androidx.compose.foundation.background
9+ import androidx.compose.foundation.gestures.Orientation
910import androidx.compose.foundation.gestures.awaitEachGesture
1011import androidx.compose.foundation.gestures.awaitFirstDown
1112import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
12- import androidx.compose.foundation.gestures.detectHorizontalDragGestures
13+ import androidx.compose.foundation.gestures.draggable
14+ import androidx.compose.foundation.gestures.rememberDraggableState
1315import androidx.compose.foundation.gestures.waitForUpOrCancellation
16+ import androidx.compose.foundation.hoverable
17+ import androidx.compose.foundation.interaction.MutableInteractionSource
18+ import androidx.compose.foundation.interaction.PressInteraction
19+ import androidx.compose.foundation.interaction.collectIsDraggedAsState
20+ import androidx.compose.foundation.interaction.collectIsHoveredAsState
21+ import androidx.compose.foundation.interaction.collectIsPressedAsState
1422import androidx.compose.foundation.layout.Box
1523import androidx.compose.foundation.layout.padding
1624import androidx.compose.foundation.layout.requiredSize
@@ -58,57 +66,46 @@ fun Switch(
5866 enabled : Boolean = true
5967) {
6068 val isChecked by rememberUpdatedState(checked)
69+
70+ val interactionSource = remember { MutableInteractionSource () }
71+ val isPressed by interactionSource.collectIsPressedAsState()
72+ val isDragged by interactionSource.collectIsDraggedAsState()
73+ val isHovered by interactionSource.collectIsHoveredAsState()
74+
6175 val hapticFeedback = LocalHapticFeedback .current
62- var hasVibrated by remember { mutableStateOf(false ) }
76+ var hasVibrated by remember { mutableStateOf(true ) }
77+ var hasVibratedOnce by remember { mutableStateOf(false ) }
78+
6379 val springSpec = remember {
6480 spring<Dp >(
6581 dampingRatio = Spring .DampingRatioLowBouncy ,
6682 stiffness = Spring .StiffnessMedium
6783 )
6884 }
69- var isPressed by remember { mutableStateOf( false ) }
85+
7086 var dragOffset by remember { mutableStateOf(0f ) }
7187 val thumbOffset by animateDpAsState(
7288 targetValue = if (isChecked) {
73- if (! enabled) 26 .dp else if (isPressed) 24 .dp else 26 .dp
89+ if (! enabled) 26 .dp else if (isPressed || isDragged || isHovered ) 24 .dp else 26 .dp
7490 } else {
75- if (! enabled) 4 .dp else if (isPressed) 3 .dp else 4 .dp
91+ if (! enabled) 4 .dp else if (isPressed || isDragged || isHovered ) 3 .dp else 4 .dp
7692 } + dragOffset.dp,
7793 animationSpec = springSpec
7894 )
7995
8096 val thumbSize by animateDpAsState(
81- targetValue = if (! enabled) 20 .dp else if (isPressed) 23 .dp else 20 .dp,
97+ targetValue = if (! enabled) 20 .dp else if (isPressed || isDragged || isHovered ) 23 .dp else 20 .dp,
8298 animationSpec = springSpec
8399 )
84100
85- val backgroundColor by animateColorAsState(
86- if (isChecked) colors.checkedTrackColor(enabled) else colors.uncheckedTrackColor(enabled),
87- animationSpec = tween(durationMillis = 200 )
88- )
89-
90101 val thumbColor by animateColorAsState(
91102 if (isChecked) colors.checkedThumbColor(enabled) else colors.uncheckedThumbColor(enabled)
92103 )
93104
94- val toggleableModifier = remember(onCheckedChange, isChecked, enabled) {
95- if (onCheckedChange != null ) {
96- Modifier .toggleable(
97- value = isChecked,
98- onValueChange = {
99- onCheckedChange(it)
100- if (it) hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOn )
101- else hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOff )
102- },
103- enabled = enabled,
104- role = Role .Switch ,
105- indication = null ,
106- interactionSource = null
107- )
108- } else {
109- Modifier
110- }
111- }
105+ val backgroundColor by animateColorAsState(
106+ if (isChecked) colors.checkedTrackColor(enabled) else colors.uncheckedTrackColor(enabled),
107+ animationSpec = tween(durationMillis = 200 )
108+ )
112109
113110 Box (
114111 modifier = modifier
@@ -117,37 +114,54 @@ fun Switch(
117114 .requiredSize(50 .dp, 28.5 .dp)
118115 .clip(RoundedCornerShape (100 .dp))
119116 .drawBehind { drawRect(backgroundColor) }
117+ .hoverable(
118+ interactionSource = interactionSource,
119+ enabled = enabled
120+ )
120121 .pointerInput(Unit ) {
121122 if (! enabled) return @pointerInput
122- detectHorizontalDragGestures(
123- onDragStart = {
124- isPressed = true
125- hasVibrated = false
126- },
127- onDragEnd = {
128- isPressed = false
129- val switchWidth = 21f
130- if (dragOffset.absoluteValue > switchWidth / 2 ) {
131- onCheckedChange?.invoke(! isChecked)
123+ val touchSlop = 16f
124+ awaitEachGesture {
125+ val down = awaitFirstDown(requireUnconsumed = false )
126+ val initialOffset = down.position
127+ var validHorizontalDrag = false
128+ do {
129+ val event = awaitPointerEvent()
130+ val currentOffset = event.changes[0 ].position
131+ val dx = (currentOffset.x - initialOffset.x).absoluteValue
132+ val dy = (currentOffset.y - initialOffset.y).absoluteValue
133+ if (dy > touchSlop) {
134+ validHorizontalDrag = false
135+ break
136+ } else if (dx > touchSlop) {
137+ validHorizontalDrag = true
132138 }
133- dragOffset = 0f
134- },
135- onDragCancel = {
136- isPressed = false
137- dragOffset = 0f
138- }
139- ) { _, dragAmount ->
140- val newOffset = dragOffset + dragAmount / 2
141- dragOffset = if (isChecked) newOffset.coerceIn(- 21f , 0f ) else newOffset.coerceIn(0f , 21f )
142- if (dragOffset in - 20f .. - 1f || dragOffset in 1f .. 20f ) {
143- hasVibrated = false
144- } else if ((dragOffset == - 21f || dragOffset == 0f || dragOffset == 21f ) && ! hasVibrated) {
145- hapticFeedback.performHapticFeedback(HapticFeedbackType .LongPress )
146- hasVibrated = true
139+ } while (event.changes.all { it.pressed })
140+
141+ if (validHorizontalDrag && ! isPressed && ! isDragged) {
142+ onCheckedChange?.invoke(! isChecked)
143+ hapticFeedback.performHapticFeedback(
144+ if (isChecked) HapticFeedbackType .ToggleOff
145+ else HapticFeedbackType .ToggleOn
146+ )
147147 }
148148 }
149149 }
150- .then(toggleableModifier)
150+ .toggleable(
151+ value = isChecked,
152+ onValueChange = {
153+ if (onCheckedChange == null ) return @toggleable
154+ onCheckedChange.invoke(it)
155+ hapticFeedback.performHapticFeedback(
156+ if (it) HapticFeedbackType .ToggleOn
157+ else HapticFeedbackType .ToggleOff
158+ )
159+ },
160+ enabled = enabled,
161+ role = Role .Switch ,
162+ indication = null ,
163+ interactionSource = null
164+ )
151165 ) {
152166 Box (
153167 modifier = Modifier
@@ -161,28 +175,58 @@ fun Switch(
161175 .pointerInput(Unit ) {
162176 if (! enabled) return @pointerInput
163177 awaitEachGesture {
164- awaitFirstDown().also {
165- it.consume()
166- isPressed = true
178+ val pressInteraction: PressInteraction .Press
179+ val down = awaitFirstDown().also {
180+ pressInteraction = PressInteraction .Press (it.position)
181+ interactionSource.tryEmit(pressInteraction)
167182 }
168- waitForUpOrCancellation()?.also {
169- it.consume()
170- isPressed = false
171- hapticFeedback.performHapticFeedback(HapticFeedbackType .LongPress )
172- onCheckedChange?.invoke(! isChecked)
183+ waitForUpOrCancellation().also {
184+ interactionSource.tryEmit(PressInteraction .Cancel (pressInteraction))
173185 }
174- }
175- }
176- .pointerInput(Unit ) {
177- if (! enabled) return @pointerInput
178- awaitEachGesture {
179- val down = awaitFirstDown(requireUnconsumed = false )
180186 awaitVerticalTouchSlopOrCancellation(down.id) { _, _ ->
181- isPressed = false
187+ interactionSource.tryEmit( PressInteraction . Cancel (pressInteraction))
182188 }
183189 }
184190 }
185-
191+ .draggable(
192+ state = rememberDraggableState { delta ->
193+ if (onCheckedChange == null ) return @rememberDraggableState
194+ dragOffset = (dragOffset + delta / 2 ).let {
195+ if (isChecked) it.coerceIn(- 21f , 0f ) else it.coerceIn(0f , 21f )
196+ }
197+ if (dragOffset in - 11f .. - 10f || dragOffset in 10f .. 11f ) {
198+ hasVibratedOnce = false
199+ } else if (dragOffset in - 20f .. - 1f || dragOffset in 1f .. 20f ) {
200+ hasVibrated = false
201+ } else if (! hasVibrated) {
202+ if ((isChecked && dragOffset == - 21f ) || (! isChecked && dragOffset == 0f )) {
203+ hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOff )
204+ hasVibrated = true
205+ hasVibratedOnce = true
206+ } else if ((isChecked && dragOffset == 0f ) || (! isChecked && dragOffset == 21f )) {
207+ hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOn )
208+ hasVibrated = true
209+ hasVibratedOnce = true
210+ }
211+ }
212+ },
213+ orientation = Orientation .Horizontal ,
214+ enabled = enabled,
215+ interactionSource = interactionSource,
216+ onDragStopped = {
217+ if (dragOffset.absoluteValue > 21f / 2 ) onCheckedChange?.invoke(! isChecked)
218+ if (! hasVibratedOnce && dragOffset.absoluteValue >= 1f ) {
219+ if ((isChecked && dragOffset <= - 11f ) || (! isChecked && dragOffset <= 10f )) {
220+ hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOff )
221+ } else if ((isChecked && dragOffset >= - 10f ) || (! isChecked && dragOffset >= 11f )) {
222+ hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOn )
223+ }
224+ }
225+ hasVibrated = true
226+ hasVibratedOnce = false
227+ dragOffset = 0f
228+ }
229+ )
186230 )
187231 }
188232}
0 commit comments