@@ -64,6 +64,7 @@ private const val DISMISS_ANIMATION_DURATION_MS = 300
6464private const val SNAP_BACK_DAMPING_RATIO = 0.7f
6565private const val DRAG_RESISTANCE_FACTOR = 0.08f
6666private const val DRAG_START_THRESHOLD_PX = 5
67+ private const val HORIZONTAL_DISMISS_ANIMATION_TARGET_PX = 500f
6768private const val TINT_ALPHA = 0.32f
6869private const val SHADOW_ALPHA = 0.4f
6970private 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