Skip to content

Commit 468a74f

Browse files
committed
ui: Support edited/moved marker swipe animation.
This adds full support to the edited/moved marker feature by allowing the user to expand the edited/moved marker to show a helper text on a colored block in the background. The marker retracts as soon as the user releases the touch. Fixes zulip#171. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 124f81f commit 468a74f

File tree

3 files changed

+158
-28
lines changed

3 files changed

+158
-28
lines changed

assets/l10n/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,5 +483,13 @@
483483
"notifSelfUser": "You",
484484
"@notifSelfUser": {
485485
"description": "Display name for the user themself, to show after replying in an Android notification"
486+
},
487+
"messageIsEdited": "Edited",
488+
"@messageIsEdited": {
489+
"description": "Text that appears on a marker next to an edited message."
490+
},
491+
"messageIsMoved": "Moved",
492+
"@messageIsMoved": {
493+
"description": "Text that appears on a marker next to a moved message."
486494
}
487495
}

lib/widgets/edit_state_marker.dart

Lines changed: 134 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import 'dart:ui';
2+
13
import 'package:flutter/material.dart';
4+
import 'package:flutter/rendering.dart';
5+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
26

37
import '../api/model/model.dart';
48
import 'icons.dart';
59
import 'message_list.dart';
610
import 'text.dart';
711

8-
class EditStateMarker extends StatelessWidget {
12+
class EditStateMarker extends StatefulWidget {
913
const EditStateMarker({
1014
super.key,
1115
required this.editState,
@@ -15,60 +19,163 @@ class EditStateMarker extends StatelessWidget {
1519
final MessageEditState editState;
1620
final List<Widget> children;
1721

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+
1856
@override
1957
Widget build(BuildContext context) {
20-
final hasMarker = editState != MessageEditState.none;
58+
final hasMarker = widget.editState != MessageEditState.none;
2159

22-
return Row(
23-
crossAxisAlignment: CrossAxisAlignment.baseline,
24-
textBaseline: localizedTextBaseline(context),
25-
children: [
26-
hasMarker
27-
? _EditStateMarkerPill(editState: 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.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+
])),
3182
);
32-
}
83+
84+
if (!hasMarker) return content;
85+
86+
return GestureDetector(
87+
onHorizontalDragEnd: _handleDragEnd,
88+
onHorizontalDragUpdate: _handleDragUpdate,
89+
child: content,
90+
);
91+
}
3392
}
3493

3594
class _EditStateMarkerPill extends StatelessWidget {
36-
const _EditStateMarkerPill({required this.editState});
95+
const _EditStateMarkerPill({required this.editState, required this.animation});
3796

3897
final MessageEditState editState;
98+
final Animation<double> animation;
3999

40100
/// 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.
43104
static const double widthCollapsed = 16;
44105

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+
45115
@override
46116
Widget build(BuildContext context) {
47117
final messageListTheme = MessageListTheme.of(context);
118+
final zulipLocalizations = ZulipLocalizations.of(context);
48119

49120
final IconData icon;
50-
final Offset offset;
121+
final String markerText;
51122
switch (editState) {
52123
case MessageEditState.none:
53124
assert(false);
54125
return const SizedBox(width: widthCollapsed);
55126
case MessageEditState.edited:
56127
icon = ZulipIcons.edited;
57-
// These offsets are chosen ad hoc, but give a good vertical alignment
58-
// of the icons with the first line of the message, when the message
59-
// begins with a paragraph, at default text scaling. See:
60-
// https://github.com/zulip/zulip-flutter/pull/762#issuecomment-2232041922
61-
offset = const Offset(0, 2);
128+
markerText = zulipLocalizations.messageIsEdited;
129+
break;
62130
case MessageEditState.moved:
63131
icon = ZulipIcons.message_moved;
64-
offset = const Offset(0, 3);
132+
markerText = zulipLocalizations.messageIsMoved;
133+
break;
65134
}
66135

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+
messageListTheme.editedMovedMarkerExpanded.withAlpha(0),
148+
messageListTheme.editedMovedMarkerExpanded,
149+
_animationProgress)))),
150+
Transform.translate(
151+
// This offset is chosen ad hoc, but give a good vertical alignment
152+
// of the icons with the first line of the message, when the message
153+
// begins with a paragraph, at default text scaling. See:
154+
// https://github.com/zulip/zulip-flutter/pull/762#issuecomment-2232041922
155+
offset: const Offset(0, 1),
156+
child: Icon(icon, size: 16, color: Color.lerp(
157+
messageListTheme.editedMovedMarkerCollapsed,
158+
messageListTheme.editedMovedMarkerExpanded,
159+
_animationProgress)),
160+
),
161+
],
162+
);
163+
67164
return ConstrainedBox(
68-
constraints: const BoxConstraints(maxWidth: widthCollapsed),
69-
child: Transform.translate(
70-
offset: offset,
71-
child: Icon(
72-
icon, size: 16, color: messageListTheme.editedMovedMarkerCollapsed)));
165+
constraints: BoxConstraints(maxWidth: animation.value),
166+
child: Container(
167+
margin: EdgeInsets.only(left: lerpDouble(0, 8, _animationProgress)!, right: lerpDouble(0, 3, _animationProgress)!),
168+
clipBehavior: Clip.hardEdge,
169+
decoration: BoxDecoration(
170+
borderRadius: BorderRadius.circular(3),
171+
color: Color.lerp(
172+
messageListTheme.editedMovedMarkerBg.withAlpha(0),
173+
messageListTheme.editedMovedMarkerBg,
174+
_animationProgress)),
175+
child: Padding(
176+
padding: EdgeInsets.only(left: lerpDouble(0, 3, _animationProgress)!),
177+
child: marker),
178+
),
179+
);
73180
}
74181
}

lib/widgets/message_list.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
3131
dateSeparator: Colors.black,
3232
dateSeparatorText: const HSLColor.fromAHSL(0.75, 0, 0, 0.15).toColor(),
3333
dmRecipientHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(),
34+
editedMovedMarkerBg: const Color(0xffddecf6),
35+
editedMovedMarkerExpanded: const Color(0xff26516e),
3436
editedMovedMarkerCollapsed: const Color.fromARGB(128, 146, 167, 182),
3537
messageTimestamp: const HSLColor.fromAHSL(0.8, 0, 0, 0.2).toColor(),
3638
recipientHeaderText: const HSLColor.fromAHSL(1, 0, 0, 0.15).toColor(),
@@ -59,6 +61,10 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
5961
dateSeparatorText: const HSLColor.fromAHSL(0.75, 0, 0, 1).toColor(),
6062
dmRecipientHeaderBg: const HSLColor.fromAHSL(1, 46, 0.15, 0.2).toColor(),
6163
// TODO(#95) need proper dark-theme color (this is ad hoc)
64+
editedMovedMarkerBg: const Color(0xffddecf6),
65+
// TODO(#95) need proper dark-theme color (this is ad hoc)
66+
editedMovedMarkerExpanded: const Color(0xff26516e),
67+
// TODO(#95) need proper dark-theme color (this is ad hoc)
6268
editedMovedMarkerCollapsed: const Color.fromARGB(128, 214, 202, 194),
6369
messageTimestamp: const HSLColor.fromAHSL(0.6, 0, 0, 1).toColor(),
6470
recipientHeaderText: const HSLColor.fromAHSL(0.8, 0, 0, 1).toColor(),
@@ -84,6 +90,8 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
8490
required this.dateSeparator,
8591
required this.dateSeparatorText,
8692
required this.dmRecipientHeaderBg,
93+
required this.editedMovedMarkerBg,
94+
required this.editedMovedMarkerExpanded,
8795
required this.editedMovedMarkerCollapsed,
8896
required this.messageTimestamp,
8997
required this.recipientHeaderText,
@@ -109,6 +117,8 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
109117
final Color dateSeparator;
110118
final Color dateSeparatorText;
111119
final Color dmRecipientHeaderBg;
120+
final Color editedMovedMarkerBg;
121+
final Color editedMovedMarkerExpanded;
112122
final Color editedMovedMarkerCollapsed;
113123
final Color messageTimestamp;
114124
final Color recipientHeaderText;
@@ -124,7 +134,8 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
124134
MessageListTheme copyWith({
125135
Color? dateSeparator,
126136
Color? dateSeparatorText,
127-
Color? dmRecipientHeaderBg,
137+
Color? editedMovedMarkerBg,
138+
Color? editedMovedMarkerExpanded,
128139
Color? editedMovedMarkerCollapsed,
129140
Color? messageTimestamp,
130141
Color? recipientHeaderText,
@@ -140,6 +151,8 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
140151
dateSeparator: dateSeparator ?? this.dateSeparator,
141152
dateSeparatorText: dateSeparatorText ?? this.dateSeparatorText,
142153
dmRecipientHeaderBg: dmRecipientHeaderBg ?? this.dmRecipientHeaderBg,
154+
editedMovedMarkerBg: editedMovedMarkerBg ?? this.editedMovedMarkerBg,
155+
editedMovedMarkerExpanded: editedMovedMarkerExpanded ?? this.editedMovedMarkerExpanded,
143156
editedMovedMarkerCollapsed: editedMovedMarkerCollapsed ?? this.editedMovedMarkerCollapsed,
144157
messageTimestamp: messageTimestamp ?? this.messageTimestamp,
145158
recipientHeaderText: recipientHeaderText ?? this.recipientHeaderText,
@@ -162,6 +175,8 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
162175
dateSeparator: Color.lerp(dateSeparator, other.dateSeparator, t)!,
163176
dateSeparatorText: Color.lerp(dateSeparatorText, other.dateSeparatorText, t)!,
164177
dmRecipientHeaderBg: Color.lerp(streamMessageBgDefault, other.dmRecipientHeaderBg, t)!,
178+
editedMovedMarkerBg: Color.lerp(editedMovedMarkerBg, other.editedMovedMarkerBg, t)!,
179+
editedMovedMarkerExpanded: Color.lerp(editedMovedMarkerExpanded, other.editedMovedMarkerExpanded, t)!,
165180
editedMovedMarkerCollapsed: Color.lerp(editedMovedMarkerCollapsed, other.editedMovedMarkerCollapsed, t)!,
166181
messageTimestamp: Color.lerp(messageTimestamp, other.messageTimestamp, t)!,
167182
recipientHeaderText: Color.lerp(recipientHeaderText, other.recipientHeaderText, t)!,

0 commit comments

Comments
 (0)