1
+ import 'dart:ui' ;
2
+
1
3
import 'package:flutter/material.dart' ;
2
4
import 'package:flutter/rendering.dart' ;
5
+ import 'package:flutter_gen/gen_l10n/zulip_localizations.dart' ;
3
6
4
7
import '../api/model/model.dart' ;
5
8
import 'icons.dart' ;
@@ -20,66 +23,165 @@ class SwipableMessageRow extends StatefulWidget {
20
23
State <StatefulWidget > createState () => _SwipableMessageRowState ();
21
24
}
22
25
23
- class _SwipableMessageRowState extends State <SwipableMessageRow > {
26
+ class _SwipableMessageRowState extends State <SwipableMessageRow > with TickerProviderStateMixin {
27
+ @override
28
+ void initState () {
29
+ super .initState ();
30
+ _controller = AnimationController (
31
+ // The duration is only used when `_controller.reverse()` is called,
32
+ // i.e.: when the drag is released and the marker gets collapsed.
33
+ duration: const Duration (milliseconds: 200 ),
34
+ lowerBound: _EditStateMarker .widthCollapsed,
35
+ upperBound: _EditStateMarker .widthExpanded,
36
+ vsync: this )
37
+ ..addListener (() => setState ((){}));
38
+ }
39
+
40
+ @override
41
+ void dispose () {
42
+ _controller.dispose ();
43
+ super .dispose ();
44
+ }
45
+
46
+ late AnimationController _controller;
47
+
48
+ void _handleDragUpdate (DragUpdateDetails details) {
49
+ _controller.value += details.delta.dx;
50
+ }
51
+
52
+ void _handleDragEnd (DragEndDetails details) {
53
+ _controller.reverse ();
54
+ }
55
+
24
56
@override
25
57
Widget build (BuildContext context) {
26
58
final hasMarker = widget.message.editState != MessageEditState .none;
27
59
28
- return Row (
29
- crossAxisAlignment: CrossAxisAlignment .baseline,
30
- textBaseline: localizedTextBaseline (context),
31
- children: [
32
- hasMarker
33
- ? _EditStateMarker (editState: widget.message.editState)
34
- : const SizedBox (width: 16 ),
35
- ...widget.children,
36
- ],
60
+ final content = LayoutBuilder (
61
+ builder: (context, constraints) => OverflowBox (
62
+ fit: OverflowBoxFit .deferToChild,
63
+ alignment: Alignment .topLeft,
64
+ maxWidth: double .infinity,
65
+ child: Row (
66
+ crossAxisAlignment: CrossAxisAlignment .baseline,
67
+ textBaseline: localizedTextBaseline (context),
68
+ children: [
69
+ hasMarker
70
+ ? _EditStateMarker (
71
+ editState: widget.message.editState,
72
+ animation: _controller)
73
+ : const SizedBox (width: 16 ),
74
+ SizedBox (
75
+ width: constraints.maxWidth - 16 ,
76
+ child: Row (
77
+ crossAxisAlignment: CrossAxisAlignment .baseline,
78
+ textBaseline: localizedTextBaseline (context),
79
+ children: widget.children),
80
+ ),
81
+ ])),
37
82
);
38
- }
83
+
84
+ if (! hasMarker) return content;
85
+
86
+ return GestureDetector (
87
+ onHorizontalDragEnd: _handleDragEnd,
88
+ onHorizontalDragUpdate: _handleDragUpdate,
89
+ child: content,
90
+ );
91
+ }
39
92
}
40
93
41
94
class _EditStateMarker extends StatelessWidget {
42
95
/// The minimum width of the marker.
43
- //
44
- // Currently, only the collapsed state of the marker has been implemented,
45
- // where only the marker icon, not the marker text, is visible.
96
+ ///
97
+ /// This is when no drag has been performed on the message row
98
+ /// where only the moved/edited icon, not the text, is visible.
46
99
static const double widthCollapsed = 16 ;
47
100
101
+ /// The maximum width of the marker.
102
+ ///
103
+ /// This is typically wider than the colored pill when the marker is fully
104
+ /// expanded. At that point only the blank space to the right of the colored
105
+ /// block will grow until the marker reaches this width.
106
+ static const double widthExpanded = 100 ;
107
+
48
108
const _EditStateMarker ({
49
109
required MessageEditState editState,
50
- }) : _editState = editState;
110
+ required Animation <double > animation,
111
+ }) : _editState = editState, _animation = animation;
51
112
52
113
final MessageEditState _editState;
114
+ final Animation <double > _animation;
115
+
116
+ double get _animationProgress => (_animation.value - widthCollapsed) / widthExpanded;
53
117
54
118
@override
55
119
Widget build (BuildContext context) {
56
120
final designVariables = DesignVariables .of (context);
121
+ final zulipLocalizations = ZulipLocalizations .of (context);
57
122
58
123
final IconData icon;
59
124
final double iconSize;
125
+ final String markerText;
60
126
61
127
switch (_editState) {
62
128
case MessageEditState .none:
63
129
return const SizedBox (width: widthCollapsed);
64
130
case MessageEditState .edited:
65
131
icon = ZulipIcons .edited;
66
132
iconSize = 14 ;
133
+ markerText = zulipLocalizations.messageIsEdited;
67
134
break ;
68
135
case MessageEditState .moved:
69
136
icon = ZulipIcons .message_moved;
70
137
iconSize = 8 ;
138
+ markerText = zulipLocalizations.messageIsMoved;
71
139
break ;
72
140
}
73
141
74
- return ConstrainedBox (
75
- constraints: const BoxConstraints (maxWidth: widthCollapsed),
76
- child: Padding (
77
- padding: const EdgeInsets .only (left: 5 , right: 3 ),
78
- child: OverflowBox (
142
+ var marker = Row (
143
+ mainAxisAlignment: MainAxisAlignment .end,
144
+ mainAxisSize: MainAxisSize .min,
145
+ children: [
146
+ Flexible (
147
+ fit: FlexFit .loose,
148
+ child: Text (markerText,
149
+ overflow: TextOverflow .clip,
150
+ softWrap: false ,
151
+ textAlign: TextAlign .center,
152
+ style: TextStyle (fontSize: 15 , color: Color .lerp (
153
+ designVariables.textMarkerExpanded.withAlpha (0 ),
154
+ designVariables.textMarkerExpanded,
155
+ _animationProgress)))),
156
+ SizedBox (width: lerpDouble (0 , 5 , _animationProgress)),
157
+ // To match the Figma design, we cannot make the collapsed width of the
158
+ // marker larger. We need to explicitly allow the icon to overflow.
159
+ OverflowBox (
79
160
fit: OverflowBoxFit .deferToChild,
80
161
maxWidth: 8 ,
81
- child: Icon (icon, size: iconSize, color: designVariables.textMarkerCollapsed)),
162
+ child: Icon (icon, size: iconSize, color: Color .lerp (
163
+ designVariables.textMarkerCollapsed,
164
+ designVariables.textMarkerExpanded,
165
+ _animationProgress)),
82
166
),
167
+ ],
168
+ );
169
+
170
+ return ConstrainedBox (
171
+ constraints: BoxConstraints (maxWidth: _animation.value),
172
+ child: Container (
173
+ margin: EdgeInsets .only (left: lerpDouble (5 , 13 , _animationProgress)! , right: 3 ),
174
+ clipBehavior: Clip .hardEdge,
175
+ decoration: BoxDecoration (
176
+ borderRadius: BorderRadius .circular (3 ),
177
+ color: Color .lerp (
178
+ designVariables.bgMarker.withAlpha (0 ),
179
+ designVariables.bgMarker,
180
+ _animationProgress)),
181
+ child: Padding (
182
+ padding: EdgeInsets .symmetric (horizontal: lerpDouble (0 , 3 , _animationProgress)! ),
183
+ child: marker),
184
+ ),
83
185
);
84
186
}
85
187
}
0 commit comments