1
+ import 'dart:ui' ;
2
+
1
3
import 'package:flutter/material.dart' ;
2
4
import 'package:flutter/rendering.dart' ;
3
5
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart' ;
@@ -20,7 +22,41 @@ class SwipableMessageRow extends StatefulWidget {
20
22
State <StatefulWidget > createState () => _SwipableMessageRowState ();
21
23
}
22
24
23
- class _SwipableMessageRowState extends State <SwipableMessageRow > {
25
+ class _SwipableMessageRowState extends State <SwipableMessageRow > with TickerProviderStateMixin {
26
+ @override
27
+ void initState () {
28
+ super .initState ();
29
+ _controller = AnimationController (
30
+ // The duration is only used when `_controller.reverse()` is called,
31
+ // i.e.: when the drag is released and the marker gets collapsed.
32
+ duration: const Duration (milliseconds: 200 ),
33
+ lowerBound: EditStateMarker .widthCollapsed,
34
+ upperBound: EditStateMarker .widthExpanded,
35
+ vsync: this )
36
+ ..addListener (() => setState ((){}));
37
+ // This controls the movement of the message content.
38
+ _slideAnimation = Tween <Offset >(begin: const Offset (0 , 0 ), end: const Offset (1 , 0 ))
39
+ .animate (_controller);
40
+ }
41
+
42
+ @override
43
+ void dispose () {
44
+ _controller.dispose ();
45
+ super .dispose ();
46
+ }
47
+
48
+ late AnimationController _controller;
49
+ late Animation <Offset > _slideAnimation;
50
+ double get dragOffset => _controller.value - EditStateMarker .widthCollapsed;
51
+
52
+ void _handleDragUpdate (DragUpdateDetails details) {
53
+ _controller.value += details.delta.dx;
54
+ }
55
+
56
+ void _handleDragEnd (DragEndDetails details) {
57
+ _controller.reverse ();
58
+ }
59
+
24
60
@override
25
61
Widget build (BuildContext context) {
26
62
final theme = Theme .of (context).extension < DesignVariables > ()! ;
@@ -32,36 +68,67 @@ class _SwipableMessageRowState extends State<SwipableMessageRow> {
32
68
Icon (ZulipIcons .star_filled, size: 16 , color: theme.starColor)]));
33
69
final hasMarker = widget.message.editState != MessageEditState .none;
34
70
35
- return Stack (
71
+ final content = Stack (
36
72
children: [
37
73
if (hasMarker) Positioned (
38
74
left: 0 ,
39
- child: EditStateMarker (editState: widget.message.editState)),
75
+ child: EditStateMarker (
76
+ editState: widget.message.editState,
77
+ animation: _controller)),
40
78
Padding (
41
- padding: const EdgeInsets .only (left: 16 , right: 16 ),
42
- child: widget.child),
79
+ // Adding [EditStateMarker.widthCollapsed] to the right padding
80
+ // cancels out the left offset applied from the initial value of
81
+ // [_slideAnimation] through [Transform.translate]. This is necessary
82
+ // before we can add a padding of 16 pixels for the star.
83
+ padding: const EdgeInsets .only (right: EditStateMarker .widthCollapsed + 16 ),
84
+ child: Transform .translate (
85
+ offset: _slideAnimation.value,
86
+ child: widget.child)),
43
87
if (widget.message.flags.contains (MessageFlag .starred))
44
88
Positioned (
45
- right: 0 ,
89
+ // Because _controller.value does not start from zero, we subtract
90
+ // its initial value from it so the star is positioned correctly
91
+ // in the beginning.
92
+ right: 0 - (_controller.value - EditStateMarker .widthCollapsed),
46
93
child: star),
47
94
],
48
95
);
96
+
97
+ if (! hasMarker) return content;
98
+
99
+ return GestureDetector (
100
+ onHorizontalDragEnd: _handleDragEnd,
101
+ onHorizontalDragUpdate: _handleDragUpdate,
102
+ child: content,
103
+ );
49
104
}
50
105
}
51
106
52
107
class EditStateMarker extends StatelessWidget {
53
108
/// The minimum width of the marker.
54
109
///
55
- /// Currently, only the collapsed state of the marker has been implemented,
56
- /// where only the marker icon, not the marker text, is visible.
110
+ /// This is when no drag has been performed on the message row
111
+ /// where only the moved/edited icon, not the text, is visible.
57
112
static const double widthCollapsed = 16 ;
58
113
114
+ /// The maximum width of the marker.
115
+ ///
116
+ /// This is typically wider than the colored pill when the marker is fully
117
+ /// expanded. At that point only the blank space to the right of the colored
118
+ /// block will grow until the marker reaches this width.
119
+ static const double widthExpanded = 100 ;
120
+
59
121
const EditStateMarker ({
60
122
super .key,
61
123
required MessageEditState editState,
62
- }) : _editState = editState;
124
+ required Animation <double > animation,
125
+ }) : _editState = editState, _animation = animation;
63
126
64
127
final MessageEditState _editState;
128
+ final Animation <double > _animation;
129
+
130
+ double get _animationProgress => _animation.value / widthExpanded;
131
+ double get _width => _animation.value;
65
132
66
133
@override
67
134
Widget build (BuildContext context) {
@@ -93,34 +160,44 @@ class EditStateMarker extends StatelessWidget {
93
160
children: [
94
161
Flexible (
95
162
fit: FlexFit .loose,
96
- // For now, [markerText] is not displayed because it is transparent and
97
- // there is not enough space in the parent ConstrainedBox .
163
+ // The trailing space serves as a padding between the marker text and
164
+ // the icon without needing another element in the row .
98
165
child: Text ('$markerText ' ,
99
166
overflow: TextOverflow .clip,
100
167
softWrap: false ,
101
168
textAlign: TextAlign .center,
102
- style: TextStyle (fontSize: 15 , color: theme.textMarker.withAlpha (0 )))),
169
+ style: TextStyle (fontSize: 15 , color: Color .lerp (
170
+ theme.textMarker.withAlpha (0 ),
171
+ theme.textMarker,
172
+ _animationProgress)))),
103
173
// To match the Figma design, we cannot make the collapsed width of the
104
174
// marker larger. We need to explicitly allow the icon to overflow.
105
175
OverflowBox (
106
176
fit: OverflowBoxFit .deferToChild,
107
177
maxWidth: 10 ,
108
- child: Icon (icon, size: iconSize, color: theme.textMarkerLight),
178
+ child: Icon (icon, size: iconSize, color: Color .lerp (
179
+ theme.textMarkerLight,
180
+ theme.textMarker,
181
+ _animationProgress)),
109
182
),
110
183
],
111
184
);
112
185
113
186
return ConstrainedBox (
114
- constraints: const BoxConstraints (maxWidth: widthCollapsed ),
187
+ constraints: BoxConstraints (maxWidth: _width ),
115
188
child: Container (
116
- margin: const EdgeInsets .only (top: 4 , left: 0 ),
189
+ margin: EdgeInsets .only (top: 4 , left: lerpDouble ( 0 , 10 , _animationProgress) ! ),
117
190
clipBehavior: Clip .hardEdge,
118
191
decoration: BoxDecoration (
119
192
borderRadius: BorderRadius .circular (2 ),
120
- color: theme.bgMarker.withAlpha (0 )),
193
+ color: Color .lerp (
194
+ theme.bgMarker.withAlpha (0 ),
195
+ theme.bgMarker,
196
+ _animationProgress)),
121
197
child: Padding (
122
198
padding: const EdgeInsets .all (1.0 ),
123
- child: marker),
199
+ child: marker,
200
+ ),
124
201
),
125
202
);
126
203
}
0 commit comments