@@ -2,20 +2,23 @@ package to.bitkit.ui.components
22
33import androidx.compose.animation.AnimatedContent
44import androidx.compose.animation.SizeTransform
5+ import androidx.compose.animation.core.Animatable
6+ import androidx.compose.animation.core.Spring
7+ import androidx.compose.animation.core.spring
8+ import androidx.compose.animation.core.tween
59import androidx.compose.animation.fadeIn
610import androidx.compose.animation.fadeOut
711import androidx.compose.animation.slideInVertically
812import androidx.compose.animation.slideOutVertically
913import androidx.compose.animation.togetherWith
1014import androidx.compose.foundation.background
15+ import androidx.compose.foundation.gestures.detectDragGestures
1116import androidx.compose.foundation.layout.Arrangement
1217import androidx.compose.foundation.layout.Box
1318import androidx.compose.foundation.layout.Column
14- import androidx.compose.foundation.layout.Row
15- import androidx.compose.foundation.layout.Spacer
1619import androidx.compose.foundation.layout.fillMaxSize
1720import androidx.compose.foundation.layout.fillMaxWidth
18- import androidx.compose.foundation.layout.height
21+ import androidx.compose.foundation.layout.offset
1922import androidx.compose.foundation.layout.padding
2023import androidx.compose.foundation.layout.size
2124import androidx.compose.foundation.layout.systemBarsPadding
@@ -26,59 +29,174 @@ import androidx.compose.material3.IconButton
2629import androidx.compose.material3.MaterialTheme
2730import androidx.compose.runtime.Composable
2831import androidx.compose.runtime.ReadOnlyComposable
32+ import androidx.compose.runtime.getValue
33+ import androidx.compose.runtime.mutableStateOf
34+ import androidx.compose.runtime.remember
35+ import androidx.compose.runtime.rememberCoroutineScope
36+ import androidx.compose.runtime.setValue
2937import androidx.compose.ui.Alignment
3038import androidx.compose.ui.Modifier
39+ import androidx.compose.ui.draw.shadow
3140import androidx.compose.ui.graphics.Color
41+ import androidx.compose.ui.input.pointer.pointerInput
3242import androidx.compose.ui.platform.testTag
3343import androidx.compose.ui.res.stringResource
3444import androidx.compose.ui.tooling.preview.Preview
45+ import androidx.compose.ui.unit.IntOffset
3546import androidx.compose.ui.unit.dp
47+ import kotlinx.coroutines.launch
3648import to.bitkit.R
3749import to.bitkit.models.Toast
3850import to.bitkit.ui.scaffold.ScreenColumn
3951import to.bitkit.ui.theme.AppThemeSurface
4052import to.bitkit.ui.theme.Colors
53+ import kotlin.math.roundToInt
4154
4255@Composable
4356fun ToastView (
4457 toast : Toast ,
4558 onDismiss : () -> Unit ,
59+ onDragStart : () -> Unit = {},
60+ onDragEnd : () -> Unit = {},
4661) {
4762 val tintColor = toast.tintColor()
63+ val coroutineScope = rememberCoroutineScope()
64+ val dragOffset = remember { Animatable (0f ) }
65+ var hasPausedAutoHide by remember { mutableStateOf(false ) }
66+ val dismissThreshold = 50 .dp
4867
4968 Box (
50- contentAlignment = Alignment .CenterStart ,
69+ contentAlignment = Alignment .TopStart ,
5170 modifier = Modifier
5271 .fillMaxWidth()
5372 .systemBarsPadding()
5473 .padding(horizontal = 16 .dp)
55- .background(tintColor.copy(alpha = 0.32f ), shape = MaterialTheme .shapes.medium)
56- .padding(16 .dp)
5774 .then(toast.testTag?.let { Modifier .testTag(it) } ? : Modifier ),
5875 ) {
59- Row (
60- verticalAlignment = Alignment .CenterVertically ,
61- modifier = Modifier .fillMaxWidth()
76+ // Main toast content
77+ Box (
78+ modifier = Modifier
79+ .fillMaxWidth()
80+ .offset { IntOffset (0 , dragOffset.value.roundToInt()) }
81+ .shadow(
82+ elevation = 10 .dp,
83+ shape = MaterialTheme .shapes.medium,
84+ ambientColor = Color .Black .copy(alpha = 0.4f ),
85+ spotColor = Color .Black .copy(alpha = 0.4f )
86+ )
87+ .background(
88+ color = MaterialTheme .colorScheme.surface.copy(alpha = 0.85f ),
89+ shape = MaterialTheme .shapes.medium
90+ )
91+ .background(
92+ color = tintColor.copy(alpha = 0.32f ),
93+ shape = MaterialTheme .shapes.medium
94+ )
95+ .pointerInput(Unit ) {
96+ detectDragGestures(
97+ onDragStart = {
98+ // Drag started
99+ },
100+ onDragEnd = {
101+ // Resume auto-hide when drag ends (if we paused it)
102+ if (hasPausedAutoHide) {
103+ hasPausedAutoHide = false
104+ onDragEnd()
105+ }
106+
107+ coroutineScope.launch {
108+ // Dismiss if swiped up enough, otherwise snap back
109+ if (dragOffset.value < - dismissThreshold.toPx()) {
110+ // Animate out
111+ dragOffset.animateTo(
112+ targetValue = - 200f ,
113+ animationSpec = tween(durationMillis = 300 )
114+ )
115+ onDismiss()
116+ } else {
117+ // Snap back to original position
118+ dragOffset.animateTo(
119+ targetValue = 0f ,
120+ animationSpec = spring(
121+ dampingRatio = 0.7f ,
122+ stiffness = Spring .StiffnessMedium
123+ )
124+ )
125+ }
126+ }
127+ },
128+ onDragCancel = {
129+ coroutineScope.launch {
130+ dragOffset.animateTo(
131+ targetValue = 0f ,
132+ animationSpec = spring(
133+ dampingRatio = 0.7f ,
134+ stiffness = Spring .StiffnessMedium
135+ )
136+ )
137+ }
138+ },
139+ onDrag = { change, dragAmount ->
140+ change.consume()
141+ coroutineScope.launch {
142+ val translation = dragOffset.value + dragAmount.y
143+
144+ if (translation < 0 ) {
145+ // Upward drag - allow freely
146+ dragOffset.snapTo(translation)
147+ } else {
148+ // Downward drag - apply resistance
149+ dragOffset.snapTo(translation * 0.08f )
150+ }
151+
152+ // Pause auto-hide when drag starts (only once)
153+ if (kotlin.math.abs(dragOffset.value) > 5 && ! hasPausedAutoHide) {
154+ hasPausedAutoHide = true
155+ onDragStart()
156+ }
157+ }
158+ }
159+ )
160+ }
62161 ) {
63- Column (modifier = Modifier .weight(1f )) {
162+ Column (
163+ verticalArrangement = Arrangement .spacedBy(2 .dp),
164+ modifier = Modifier
165+ .fillMaxWidth()
166+ .padding(16 .dp)
167+ ) {
64168 BodyMSB (
65169 text = toast.title,
66170 color = tintColor,
67171 )
68172 toast.description?.let { description ->
69- Spacer (modifier = Modifier .height(8 .dp))
70- Caption (text = description)
173+ Caption (
174+ text = description,
175+ color = MaterialTheme .colorScheme.onSurface
176+ )
71177 }
72178 }
73- if (! toast.autoHide) {
179+ }
180+
181+ // Close button overlay (top-trailing)
182+ if (! toast.autoHide) {
183+ Box (
184+ modifier = Modifier
185+ .fillMaxWidth()
186+ .offset { IntOffset (0 , dragOffset.value.roundToInt()) },
187+ contentAlignment = Alignment .TopEnd
188+ ) {
74189 IconButton (
75190 onClick = onDismiss,
76- modifier = Modifier .size(24 .dp)
191+ modifier = Modifier
192+ .size(48 .dp)
193+ .padding(16 .dp)
77194 ) {
78195 Icon (
79196 imageVector = Icons .Default .Close ,
80197 contentDescription = stringResource(R .string.common__close),
81- tint = Color .White ,
198+ tint = MaterialTheme .colorScheme.onSurfaceVariant,
199+ modifier = Modifier .size(16 .dp)
82200 )
83201 }
84202 }
@@ -90,6 +208,8 @@ fun ToastView(
90208private fun ToastHost (
91209 toast : Toast ? ,
92210 onDismiss : () -> Unit ,
211+ onDragStart : () -> Unit = {},
212+ onDragEnd : () -> Unit = {},
93213) {
94214 AnimatedContent (
95215 targetState = toast,
@@ -102,7 +222,12 @@ private fun ToastHost(
102222 label = " toastAnimation" ,
103223 ) {
104224 if (it != null ) {
105- ToastView (toast = it, onDismiss = onDismiss)
225+ ToastView (
226+ toast = it,
227+ onDismiss = onDismiss,
228+ onDragStart = onDragStart,
229+ onDragEnd = onDragEnd
230+ )
106231 }
107232 }
108233}
@@ -112,12 +237,19 @@ fun ToastOverlay(
112237 toast : Toast ? ,
113238 modifier : Modifier = Modifier ,
114239 onDismiss : () -> Unit ,
240+ onDragStart : () -> Unit = {},
241+ onDragEnd : () -> Unit = {},
115242) {
116243 Box (
117244 contentAlignment = Alignment .TopCenter ,
118245 modifier = modifier.fillMaxSize(),
119246 ) {
120- ToastHost (toast = toast, onDismiss = onDismiss)
247+ ToastHost (
248+ toast = toast,
249+ onDismiss = onDismiss,
250+ onDragStart = onDragStart,
251+ onDragEnd = onDragEnd
252+ )
121253 }
122254}
123255
0 commit comments