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