Skip to content

Commit e6b9b47

Browse files
authored
library: Optimize Switch interactions (#57)
* library: Optimize Switch interactions * library: Fix Switch sensitivity in horizontal
1 parent 48fb58a commit e6b9b47

File tree

1 file changed

+117
-66
lines changed
  • miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic

1 file changed

+117
-66
lines changed

miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Switch.kt

Lines changed: 117 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import androidx.compose.foundation.gestures.awaitFirstDown
1111
import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
1212
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
1313
import 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
1421
import androidx.compose.foundation.layout.Box
1522
import androidx.compose.foundation.layout.padding
1623
import 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

Comments
 (0)