@@ -8,6 +8,7 @@ import androidx.compose.foundation.gestures.AnchoredDraggableState
8
8
import androidx.compose.foundation.gestures.DraggableAnchors
9
9
import androidx.compose.foundation.gestures.Orientation
10
10
import androidx.compose.foundation.gestures.anchoredDraggable
11
+ import androidx.compose.foundation.gestures.animateTo
11
12
import androidx.compose.foundation.layout.Arrangement
12
13
import androidx.compose.foundation.layout.Box
13
14
import androidx.compose.foundation.layout.Row
@@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.size
17
18
import androidx.compose.material3.Icon
18
19
import androidx.compose.runtime.Composable
19
20
import androidx.compose.runtime.CompositionLocalProvider
21
+ import androidx.compose.runtime.LaunchedEffect
20
22
import androidx.compose.runtime.Stable
21
23
import androidx.compose.runtime.getValue
22
24
import androidx.compose.runtime.mutableStateOf
@@ -40,26 +42,60 @@ import androidx.compose.ui.unit.dp
40
42
import com.wire.android.R
41
43
import com.wire.android.ui.common.colorsScheme
42
44
import com.wire.android.ui.common.dimensions
43
- import com.wire.android.ui.home.conversations.model.UIMessage
44
45
import kotlin.math.absoluteValue
45
46
import kotlin.math.min
46
47
47
48
@Stable
48
- sealed interface SwipableMessageConfiguration {
49
- data object NotSwipable : SwipableMessageConfiguration
50
- class SwipableToReply (val onSwipedToReply : (uiMessage: UIMessage .Regular ) -> Unit ) : SwipableMessageConfiguration
49
+ sealed interface SwipeableMessageConfiguration {
50
+ data object NotSwipeable : SwipeableMessageConfiguration
51
+ class Swipeable (
52
+ val onSwipedRight : (() -> Unit )? = null ,
53
+ val onSwipedLeft : (() -> Unit )? = null ,
54
+ ) : SwipeableMessageConfiguration
51
55
}
52
56
53
57
enum class SwipeAnchor {
54
58
CENTERED ,
55
- START_TO_END
59
+ START_TO_END ,
60
+ END_TO_START ,
56
61
}
57
62
63
+ data class SwipeAction (
64
+ val icon : Int ,
65
+ val action : () -> Unit ,
66
+ )
67
+
68
+ @Composable
69
+ internal fun SwipeableMessageBox (
70
+ configuration : SwipeableMessageConfiguration .Swipeable ,
71
+ modifier : Modifier = Modifier ,
72
+ content : @Composable () -> Unit ,
73
+ ) {
74
+ SwipeableBox (
75
+ modifier = modifier,
76
+ onSwipeRight = configuration.onSwipedRight?.let {
77
+ SwipeAction (
78
+ icon = R .drawable.ic_reply,
79
+ action = it,
80
+ )
81
+ },
82
+ onSwipeLeft = configuration.onSwipedLeft?.let {
83
+ SwipeAction (
84
+ icon = R .drawable.ic_react,
85
+ action = it,
86
+ )
87
+ },
88
+ content = content,
89
+ )
90
+ }
91
+
92
+ @Suppress(" CyclomaticComplexMethod" )
58
93
@OptIn(ExperimentalFoundationApi ::class )
59
94
@Composable
60
- internal fun SwipableToReplyBox (
95
+ private fun SwipeableBox (
61
96
modifier : Modifier = Modifier ,
62
- onSwipedToReply : () -> Unit = {},
97
+ onSwipeRight : SwipeAction ? = null,
98
+ onSwipeLeft : SwipeAction ? = null,
63
99
content : @Composable () -> Unit
64
100
) {
65
101
val density = LocalDensity .current
@@ -86,55 +122,83 @@ internal fun SwipableToReplyBox(
86
122
velocityThreshold = { screenWidth },
87
123
snapAnimationSpec = tween(),
88
124
decayAnimationSpec = splineBasedDecay(density),
89
- confirmValueChange = { changedValue ->
90
- if (changedValue == SwipeAnchor .START_TO_END ) {
91
- // Attempt to finish dismiss, notify reply intention
92
- onSwipedToReply()
93
- }
94
- if (changedValue == SwipeAnchor .CENTERED ) {
95
- // Reset the haptic feedback when drag is stopped
96
- didVibrateOnCurrentDrag = false
97
- }
98
- // Reject state change, only allow returning back to rest position
99
- changedValue == SwipeAnchor .CENTERED
100
- },
101
125
anchors = DraggableAnchors {
126
+
102
127
SwipeAnchor .CENTERED at 0f
103
- SwipeAnchor .START_TO_END at screenWidth
128
+
129
+ if (onSwipeRight != null ) {
130
+ SwipeAnchor .START_TO_END at dragWidth
131
+ }
132
+
133
+ if (onSwipeLeft != null ) {
134
+ SwipeAnchor .END_TO_START at - dragWidth
135
+ }
104
136
}
105
137
)
106
138
}
139
+
140
+ LaunchedEffect (dragState.settledValue) {
141
+ when (dragState.settledValue) {
142
+ SwipeAnchor .START_TO_END -> {
143
+ onSwipeRight?.action?.invoke()
144
+ dragState.animateTo(SwipeAnchor .CENTERED )
145
+ }
146
+
147
+ SwipeAnchor .END_TO_START -> {
148
+ onSwipeLeft?.action?.invoke()
149
+ dragState.animateTo(SwipeAnchor .CENTERED )
150
+ }
151
+
152
+ SwipeAnchor .CENTERED -> {}
153
+ }
154
+ didVibrateOnCurrentDrag = false
155
+ }
156
+
107
157
val primaryColor = colorsScheme().primary
108
158
109
159
Box (
110
160
modifier = modifier.fillMaxSize(),
111
161
) {
162
+
163
+ val dragOffset = dragState.requireOffset()
164
+
112
165
// Drag indication
113
166
Row (
114
167
modifier = Modifier
115
168
.matchParentSize()
116
169
.drawBehind {
117
- // TODO(RTL): Might need adjusting once RTL is supported
118
170
drawRect(
119
171
color = primaryColor,
120
- topLeft = Offset (0f , 0f ),
121
- size = Size (dragState.requireOffset().absoluteValue, size.height),
172
+ topLeft = if (dragOffset >= 0f ) {
173
+ Offset (0f , 0f )
174
+ } else {
175
+ Offset (size.width - dragOffset.absoluteValue, 0f )
176
+ },
177
+ size = Size (dragOffset.absoluteValue, size.height),
122
178
)
123
179
},
124
180
verticalAlignment = Alignment .CenterVertically ,
125
181
horizontalArrangement = Arrangement .Start
126
182
) {
183
+
184
+ val dragProgress = dragState.offset.absoluteValue / dragWidth
185
+ val adjustedProgress = min(1f , dragProgress)
186
+ val progress = FastOutLinearInEasing .transform(adjustedProgress)
187
+
188
+ // Got to the end, user can release to perform action, so we vibrate to show it
189
+ if (progress == 1f && ! didVibrateOnCurrentDrag) {
190
+ haptic.performHapticFeedback(HapticFeedbackType .LongPress )
191
+ didVibrateOnCurrentDrag = true
192
+ }
193
+
127
194
if (dragState.offset > 0f ) {
128
- val dragProgress = dragState.offset / dragWidth
129
- val adjustedProgress = min(1f , dragProgress)
130
- val progress = FastOutLinearInEasing .transform(adjustedProgress)
131
- // Got to the end, user can release to perform action, so we vibrate to show it
132
- if (progress == 1f && ! didVibrateOnCurrentDrag) {
133
- haptic.performHapticFeedback(HapticFeedbackType .LongPress )
134
- didVibrateOnCurrentDrag = true
195
+ onSwipeRight?.let { action ->
196
+ SwipeActionIcon (action.icon, screenWidth, dragWidth, density, progress)
197
+ }
198
+ } else if (dragState.offset < 0f ) {
199
+ onSwipeLeft?.let {
200
+ SwipeActionIcon (it.icon, screenWidth, dragWidth, density, progress, false )
135
201
}
136
-
137
- ReplySwipeIcon (dragWidth, density, progress)
138
202
}
139
203
}
140
204
// Message content, which is draggable
@@ -154,20 +218,37 @@ internal fun SwipableToReplyBox(
154
218
}
155
219
156
220
@Composable
157
- private fun ReplySwipeIcon (dragWidth : Float , density : Density , progress : Float ) {
221
+ private fun SwipeActionIcon (
222
+ resourceId : Int ,
223
+ screenWidth : Float ,
224
+ dragWidth : Float ,
225
+ density : Density ,
226
+ progress : Float ,
227
+ swipeRight : Boolean = true
228
+ ) {
158
229
val midPointBetweenStartAndGestureEnd = dragWidth / 2
159
230
val iconSize = dimensions().fabIconSize
160
231
val targetIconAnchorPosition = midPointBetweenStartAndGestureEnd - with (density) { iconSize.toPx() / 2 }
161
232
val xOffset = with (density) {
162
233
val totalTravelDistance = iconSize.toPx() + targetIconAnchorPosition
163
- - iconSize.toPx() + (totalTravelDistance * progress)
234
+ if (swipeRight) {
235
+ (totalTravelDistance * progress) - iconSize.toPx()
236
+ } else {
237
+ (totalTravelDistance * progress) - iconSize.toPx() / 2
238
+ }
164
239
}
165
240
Icon (
166
- painter = painterResource(id = R .drawable.ic_reply ),
241
+ painter = painterResource(id = resourceId ),
167
242
contentDescription = " " ,
168
243
modifier = Modifier
169
244
.size(iconSize)
170
- .offset { IntOffset (xOffset.toInt(), 0 ) },
245
+ .offset {
246
+ if (swipeRight) {
247
+ IntOffset (xOffset.toInt(), 0 )
248
+ } else {
249
+ IntOffset (screenWidth.toInt() - xOffset.toInt(), 0 )
250
+ }
251
+ },
171
252
tint = colorsScheme().onPrimary
172
253
)
173
254
}
0 commit comments