@@ -11,6 +11,13 @@ import androidx.compose.foundation.gestures.awaitFirstDown
1111import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
1212import androidx.compose.foundation.gestures.detectHorizontalDragGestures
1313import androidx.compose.foundation.gestures.waitForUpOrCancellation
14+ import androidx.compose.foundation.hoverable
15+ import androidx.compose.foundation.interaction.DragInteraction
16+ import androidx.compose.foundation.interaction.MutableInteractionSource
17+ import androidx.compose.foundation.interaction.PressInteraction
18+ import androidx.compose.foundation.interaction.collectIsDraggedAsState
19+ import androidx.compose.foundation.interaction.collectIsHoveredAsState
20+ import androidx.compose.foundation.interaction.collectIsPressedAsState
1421import androidx.compose.foundation.layout.Box
1522import androidx.compose.foundation.layout.padding
1623import androidx.compose.foundation.layout.requiredSize
@@ -58,57 +65,46 @@ fun Switch(
5865 enabled : Boolean = true
5966) {
6067 val isChecked by rememberUpdatedState(checked)
68+
69+ val interactionSource = remember { MutableInteractionSource () }
70+ val isPressed by interactionSource.collectIsPressedAsState()
71+ val isDragged by interactionSource.collectIsDraggedAsState()
72+ val isHovered by interactionSource.collectIsHoveredAsState()
73+
6174 val hapticFeedback = LocalHapticFeedback .current
62- var hasVibrated by remember { mutableStateOf(false ) }
75+ var hasVibrated by remember { mutableStateOf(true ) }
76+ var hasVibratedOnce by remember { mutableStateOf(false ) }
77+
6378 val springSpec = remember {
6479 spring<Dp >(
6580 dampingRatio = Spring .DampingRatioLowBouncy ,
6681 stiffness = Spring .StiffnessMedium
6782 )
6883 }
69- var isPressed by remember { mutableStateOf( false ) }
84+
7085 var dragOffset by remember { mutableStateOf(0f ) }
7186 val thumbOffset by animateDpAsState(
7287 targetValue = if (isChecked) {
73- if (! enabled) 26 .dp else if (isPressed) 24 .dp else 26 .dp
88+ if (! enabled) 26 .dp else if (isPressed || isDragged || isHovered ) 24 .dp else 26 .dp
7489 } else {
75- if (! enabled) 4 .dp else if (isPressed) 3 .dp else 4 .dp
90+ if (! enabled) 4 .dp else if (isPressed || isDragged || isHovered ) 3 .dp else 4 .dp
7691 } + dragOffset.dp,
7792 animationSpec = springSpec
7893 )
7994
8095 val thumbSize by animateDpAsState(
81- targetValue = if (! enabled) 20 .dp else if (isPressed) 23 .dp else 20 .dp,
96+ targetValue = if (! enabled) 20 .dp else if (isPressed || isDragged || isHovered ) 23 .dp else 20 .dp,
8297 animationSpec = springSpec
8398 )
8499
85- val backgroundColor by animateColorAsState(
86- if (isChecked) colors.checkedTrackColor(enabled) else colors.uncheckedTrackColor(enabled),
87- animationSpec = tween(durationMillis = 200 )
88- )
89-
90100 val thumbColor by animateColorAsState(
91101 if (isChecked) colors.checkedThumbColor(enabled) else colors.uncheckedThumbColor(enabled)
92102 )
93103
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- }
104+ val backgroundColor by animateColorAsState(
105+ if (isChecked) colors.checkedTrackColor(enabled) else colors.uncheckedTrackColor(enabled),
106+ animationSpec = tween(durationMillis = 200 )
107+ )
112108
113109 Box (
114110 modifier = modifier
@@ -117,37 +113,54 @@ fun Switch(
117113 .requiredSize(50 .dp, 28.5 .dp)
118114 .clip(RoundedCornerShape (100 .dp))
119115 .drawBehind { drawRect(backgroundColor) }
116+ .hoverable(
117+ interactionSource = interactionSource,
118+ enabled = enabled
119+ )
120120 .pointerInput(Unit ) {
121121 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)
122+ val touchSlop = 16f
123+ awaitEachGesture {
124+ val down = awaitFirstDown(requireUnconsumed = false )
125+ val initialOffset = down.position
126+ var validHorizontalDrag = false
127+ do {
128+ val event = awaitPointerEvent()
129+ val currentOffset = event.changes[0 ].position
130+ val dx = (currentOffset.x - initialOffset.x).absoluteValue
131+ val dy = (currentOffset.y - initialOffset.y).absoluteValue
132+ if (dy > touchSlop) {
133+ validHorizontalDrag = false
134+ break
135+ } else if (dx > touchSlop) {
136+ validHorizontalDrag = true
132137 }
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
138+ } while (event.changes.all { it.pressed })
139+
140+ if (validHorizontalDrag && ! isPressed && ! isDragged) {
141+ onCheckedChange?.invoke(! isChecked)
142+ hapticFeedback.performHapticFeedback(
143+ if (isChecked) HapticFeedbackType .ToggleOff
144+ else HapticFeedbackType .ToggleOn
145+ )
147146 }
148147 }
149148 }
150- .then(toggleableModifier)
149+ .toggleable(
150+ value = isChecked,
151+ onValueChange = {
152+ if (onCheckedChange == null ) return @toggleable
153+ onCheckedChange.invoke(it)
154+ hapticFeedback.performHapticFeedback(
155+ if (it) HapticFeedbackType .ToggleOn
156+ else HapticFeedbackType .ToggleOff
157+ )
158+ },
159+ enabled = enabled,
160+ role = Role .Switch ,
161+ indication = null ,
162+ interactionSource = null
163+ )
151164 ) {
152165 Box (
153166 modifier = Modifier
@@ -161,28 +174,66 @@ fun Switch(
161174 .pointerInput(Unit ) {
162175 if (! enabled) return @pointerInput
163176 awaitEachGesture {
164- awaitFirstDown().also {
165- it.consume()
166- isPressed = true
177+ val pressInteraction: PressInteraction .Press
178+ val down = awaitFirstDown().also {
179+ pressInteraction = PressInteraction .Press (it.position)
180+ interactionSource.tryEmit(pressInteraction)
167181 }
168- waitForUpOrCancellation()? .also {
169- it.consume( )
170- isPressed = false
171- hapticFeedback.performHapticFeedback( HapticFeedbackType . LongPress )
172- onCheckedChange?.invoke( ! isChecked )
182+ waitForUpOrCancellation().also {
183+ interactionSource.tryEmit( PressInteraction . Cancel (pressInteraction) )
184+ }
185+ awaitVerticalTouchSlopOrCancellation(down.id) { _, _ ->
186+ interactionSource.tryEmit( PressInteraction . Cancel (pressInteraction) )
173187 }
174188 }
175189 }
176190 .pointerInput(Unit ) {
177191 if (! enabled) return @pointerInput
178- awaitEachGesture {
179- val down = awaitFirstDown(requireUnconsumed = false )
180- awaitVerticalTouchSlopOrCancellation(down.id) { _, _ ->
181- isPressed = false
192+ val dragInteraction: DragInteraction .Start = DragInteraction .Start ()
193+ detectHorizontalDragGestures(
194+ onDragStart = {
195+ interactionSource.tryEmit(dragInteraction)
196+ hasVibrated = false
197+ },
198+ onDragEnd = {
199+ if (dragOffset.absoluteValue > 21f / 2 ) onCheckedChange?.invoke(! isChecked)
200+ if (! hasVibratedOnce && dragOffset.absoluteValue >= 1f ) {
201+ if ((isChecked && dragOffset <= - 11f ) || (! isChecked && dragOffset <= 10f )) {
202+ hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOff )
203+ } else if ((isChecked && dragOffset >= - 10f ) || (! isChecked && dragOffset >= 11f )) {
204+ hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOn )
205+ }
206+ }
207+ interactionSource.tryEmit(DragInteraction .Stop (dragInteraction))
208+ hasVibrated = true
209+ hasVibratedOnce = false
210+ dragOffset = 0f
211+ },
212+ onDragCancel = {
213+ interactionSource.tryEmit(DragInteraction .Cancel (dragInteraction))
214+ dragOffset = 0f
215+ }
216+ ) { _, dragAmount ->
217+ dragOffset = (dragOffset + dragAmount / 2 ).let {
218+ if (isChecked) it.coerceIn(- 21f , 0f ) else it.coerceIn(0f , 21f )
219+ }
220+ if (dragOffset in - 11f .. - 10f || dragOffset in 10f .. 11f ) {
221+ hasVibratedOnce = false
222+ } else if (dragOffset in - 20f .. - 1f || dragOffset in 1f .. 20f ) {
223+ hasVibrated = false
224+ } else if (! hasVibrated) {
225+ if ((isChecked && dragOffset == - 21f ) || (! isChecked && dragOffset == 0f )) {
226+ hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOff )
227+ hasVibrated = true
228+ hasVibratedOnce = true
229+ } else if ((isChecked && dragOffset == 0f ) || (! isChecked && dragOffset == 21f )) {
230+ hapticFeedback.performHapticFeedback(HapticFeedbackType .ToggleOn )
231+ hasVibrated = true
232+ hasVibratedOnce = true
233+ }
182234 }
183235 }
184236 }
185-
186237 )
187238 }
188239}
0 commit comments