Skip to content

Commit 3a05d47

Browse files
committed
feat: add horizontal swipe detector
1 parent 0765eb4 commit 3a05d47

File tree

1 file changed

+73
-24
lines changed

1 file changed

+73
-24
lines changed

app/src/main/java/to/bitkit/ui/components/ToastView.kt

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ private const val DISMISS_ANIMATION_DURATION_MS = 300
6464
private const val SNAP_BACK_DAMPING_RATIO = 0.7f
6565
private const val DRAG_RESISTANCE_FACTOR = 0.08f
6666
private const val DRAG_START_THRESHOLD_PX = 5
67+
private const val HORIZONTAL_DISMISS_ANIMATION_TARGET_PX = 500f
6768
private const val TINT_ALPHA = 0.32f
6869
private const val SHADOW_ALPHA = 0.4f
6970
private const val ELEVATION_DP = 10
@@ -81,7 +82,8 @@ fun ToastView(
8182
) {
8283
val tintColor = toast.tintColor()
8384
val coroutineScope = rememberCoroutineScope()
84-
val dragOffset = remember { Animatable(0f) }
85+
val dragOffsetY = remember { Animatable(0f) }
86+
val dragOffsetX = remember { Animatable(0f) }
8587
var hasPausedAutoHide by remember { mutableStateOf(false) }
8688
val dismissThreshold = DISMISS_THRESHOLD_DP.dp
8789

@@ -97,7 +99,7 @@ fun ToastView(
9799
Box(
98100
modifier = Modifier
99101
.fillMaxWidth()
100-
.offset { IntOffset(0, dragOffset.value.roundToInt()) }
102+
.offset { IntOffset(dragOffsetX.value.roundToInt(), dragOffsetY.value.roundToInt()) }
101103
.shadow(
102104
elevation = ELEVATION_DP.dp,
103105
shape = MaterialTheme.shapes.medium,
@@ -122,52 +124,99 @@ fun ToastView(
122124
}
123125

124126
coroutineScope.launch {
125-
// Dismiss if swiped up enough, otherwise snap back
126-
if (dragOffset.value < -dismissThreshold.toPx()) {
127-
// Animate out
128-
dragOffset.animateTo(
127+
val horizontalSwipeDistance = kotlin.math.abs(dragOffsetX.value)
128+
val verticalSwipeDistance = kotlin.math.abs(dragOffsetY.value)
129+
130+
// Determine if this is primarily horizontal or vertical swipe
131+
val isHorizontalSwipe = horizontalSwipeDistance > verticalSwipeDistance
132+
133+
if (isHorizontalSwipe && horizontalSwipeDistance > dismissThreshold.toPx()) {
134+
// Horizontal swipe dismiss - animate out in swipe direction
135+
val targetX = if (dragOffsetX.value > 0) {
136+
HORIZONTAL_DISMISS_ANIMATION_TARGET_PX
137+
} else {
138+
-HORIZONTAL_DISMISS_ANIMATION_TARGET_PX
139+
}
140+
dragOffsetX.animateTo(
141+
targetValue = targetX,
142+
animationSpec = tween(durationMillis = DISMISS_ANIMATION_DURATION_MS)
143+
)
144+
onDismiss()
145+
} else if (!isHorizontalSwipe && dragOffsetY.value < -dismissThreshold.toPx()) {
146+
// Vertical swipe up dismiss - animate out upward
147+
dragOffsetY.animateTo(
129148
targetValue = DISMISS_ANIMATION_TARGET_PX,
130149
animationSpec = tween(durationMillis = DISMISS_ANIMATION_DURATION_MS)
131150
)
132151
onDismiss()
133152
} else {
134153
// Snap back to original position
135-
dragOffset.animateTo(
154+
launch {
155+
dragOffsetX.animateTo(
156+
targetValue = 0f,
157+
animationSpec = spring(
158+
dampingRatio = SNAP_BACK_DAMPING_RATIO,
159+
stiffness = Spring.StiffnessMedium
160+
)
161+
)
162+
}
163+
launch {
164+
dragOffsetY.animateTo(
165+
targetValue = 0f,
166+
animationSpec = spring(
167+
dampingRatio = SNAP_BACK_DAMPING_RATIO,
168+
stiffness = Spring.StiffnessMedium
169+
)
170+
)
171+
}
172+
}
173+
}
174+
},
175+
onDragCancel = {
176+
coroutineScope.launch {
177+
launch {
178+
dragOffsetX.animateTo(
136179
targetValue = 0f,
137180
animationSpec = spring(
138181
dampingRatio = SNAP_BACK_DAMPING_RATIO,
139182
stiffness = Spring.StiffnessMedium
140183
)
141184
)
142185
}
143-
}
144-
},
145-
onDragCancel = {
146-
coroutineScope.launch {
147-
dragOffset.animateTo(
148-
targetValue = 0f,
149-
animationSpec = spring(
150-
dampingRatio = SNAP_BACK_DAMPING_RATIO,
151-
stiffness = Spring.StiffnessMedium
186+
launch {
187+
dragOffsetY.animateTo(
188+
targetValue = 0f,
189+
animationSpec = spring(
190+
dampingRatio = SNAP_BACK_DAMPING_RATIO,
191+
stiffness = Spring.StiffnessMedium
192+
)
152193
)
153-
)
194+
}
154195
}
155196
},
156197
onDrag = { change, dragAmount ->
157198
change.consume()
158199
coroutineScope.launch {
159-
val translation = dragOffset.value + dragAmount.y
160-
161-
if (translation < 0) {
200+
// Handle vertical drag
201+
val translationY = dragOffsetY.value + dragAmount.y
202+
if (translationY < 0) {
162203
// Upward drag - allow freely
163-
dragOffset.snapTo(translation)
204+
dragOffsetY.snapTo(translationY)
164205
} else {
165206
// Downward drag - apply resistance
166-
dragOffset.snapTo(translation * DRAG_RESISTANCE_FACTOR)
207+
dragOffsetY.snapTo(translationY * DRAG_RESISTANCE_FACTOR)
167208
}
168209

210+
// Handle horizontal drag - allow freely in both directions
211+
val translationX = dragOffsetX.value + dragAmount.x
212+
dragOffsetX.snapTo(translationX)
213+
169214
// Pause auto-hide when drag starts (only once)
170-
if (kotlin.math.abs(dragOffset.value) > DRAG_START_THRESHOLD_PX && !hasPausedAutoHide) {
215+
val totalDragDistance = kotlin.math.sqrt(
216+
dragOffsetX.value * dragOffsetX.value +
217+
dragOffsetY.value * dragOffsetY.value
218+
)
219+
if (totalDragDistance > DRAG_START_THRESHOLD_PX && !hasPausedAutoHide) {
171220
hasPausedAutoHide = true
172221
onDragStart()
173222
}
@@ -200,7 +249,7 @@ fun ToastView(
200249
Box(
201250
modifier = Modifier
202251
.fillMaxWidth()
203-
.offset { IntOffset(0, dragOffset.value.roundToInt()) },
252+
.offset { IntOffset(dragOffsetX.value.roundToInt(), dragOffsetY.value.roundToInt()) },
204253
contentAlignment = Alignment.TopEnd
205254
) {
206255
IconButton(

0 commit comments

Comments
 (0)