fix reaction bubble ui

This commit is contained in:
Salvatore Giordano
2020-10-15 10:37:22 +02:00
parent 2780936de3
commit 069f1a8ee4
7 changed files with 313 additions and 239 deletions
+66 -220
View File
@@ -2,12 +2,13 @@ import 'dart:math';
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/message_actions_modal.dart';
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'image_group.dart';
@@ -198,9 +199,6 @@ class MessageWidget extends StatefulWidget {
}
class _MessageWidgetState extends State<MessageWidget> {
final GlobalKey _reactionPickerKey = GlobalKey();
double _reactionPadding = 0;
@override
Widget build(BuildContext context) {
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
@@ -243,11 +241,15 @@ class _MessageWidgetState extends State<MessageWidget> {
child: Padding(
padding: widget.showReactions
? EdgeInsets.only(
top: _reactionPadding,
top: widget.message.reactionCounts
?.isNotEmpty ==
true
? 16
: 0,
)
: EdgeInsets.zero,
child: PortalEntry(
portalAnchor: Alignment(0, 1),
portalAnchor: Alignment(-0.81, 0),
childAnchor: Alignment.topRight,
portal: _buildReactionIndicator(context),
child: (widget.message.isDeleted &&
@@ -296,57 +298,6 @@ class _MessageWidgetState extends State<MessageWidget> {
);
}
@override
void didUpdateWidget(MessageWidget oldWidget) {
super.didUpdateWidget(oldWidget);
_updateReactionPadding();
}
@override
void initState() {
super.initState();
_updateReactionPadding();
}
void _updateReactionPadding() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (!mounted) {
return;
}
if (_reactionPickerKey.currentContext != null &&
widget.message.reactionCounts != null &&
widget.message.reactionCounts.values
.where((element) => element > 0)
.isNotEmpty) {
setState(() {
_reactionPadding = _reactionPickerKey.currentContext.size.height;
});
} else {
setState(() {
_reactionPadding = 0;
});
}
});
}
Widget _buildReactionsTail(BuildContext context) {
return AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: widget.message.reactionCounts?.isNotEmpty == true
? Transform.translate(
offset: Offset(4, 0),
child: CustomPaint(
painter: ReactionBubblePainter(
widget.messageTheme.reactionsBackgroundColor,
widget.messageTheme.reactionsBorderColor,
widget.message.reactionCounts.length,
),
),
)
: SizedBox(),
);
}
Padding _buildBottomRow(double leftPadding) {
return Padding(
padding: EdgeInsets.only(
@@ -427,53 +378,72 @@ class _MessageWidgetState extends State<MessageWidget> {
}
Widget _buildReactionIndicator(BuildContext context) {
final otherReactions = widget.message.latestReactions
?.where(
(element) => element.user.id != StreamChat.of(context).user.id)
?.toList() ??
[];
var rowChildren = [
if (widget.message.ownReactions?.isNotEmpty == true) ...[
Transform.translate(
offset: Offset(
widget.reverse ? -6 : 0,
0,
),
child: ReactionBubble(
key: ValueKey('${widget.message.id}.own'),
reverse: widget.reverse,
backgroundColor: widget.messageTheme.ownReactionsBackgroundColor,
borderColor: widget.messageTheme.ownReactionsBorderColor,
reactions: widget.message.ownReactions,
),
),
if (otherReactions.isEmpty)
Container(
width: 18,
),
],
if (otherReactions?.isNotEmpty == true) ...[
if (widget.message.ownReactions?.isEmpty == true)
Container(
width: 18,
),
Transform.translate(
offset: Offset(
widget.reverse ? 0 : -6,
0,
),
child: ReactionBubble(
key: ValueKey('${widget.message.id}.other'),
reactions: otherReactions,
reverse: widget.reverse,
flipTail: true,
backgroundColor: widget.messageTheme.otherReactionsBackgroundColor,
borderColor: widget.messageTheme.otherReactionsBorderColor,
),
),
]
];
if (widget.reverse) {
rowChildren = rowChildren.reversed.toList();
}
return AnimatedSwitcher(
key: _reactionPickerKey,
duration: Duration(milliseconds: 300),
child: (widget.showReactions &&
widget.message.reactionCounts?.isNotEmpty == true &&
(widget.message.reactionCounts?.isNotEmpty == true ||
otherReactions.isNotEmpty) &&
!widget.message.isDeleted)
? Container(
child: GestureDetector(
onTap: () => onLongPress(context),
child: FractionallySizedBox(
widthFactor: 0.3,
child: Padding(
padding: const EdgeInsets.only(
bottom: 4.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: widget.messageTheme
.reactionsBackgroundColor ==
StreamChatTheme.of(context)
.backgroundColor
? Border.all(
color: Theme.of(context).brightness ==
Brightness.dark
? Colors.white.withAlpha(24)
: Colors.black.withAlpha(24),
)
: null,
color:
widget.messageTheme.reactionsBackgroundColor,
borderRadius:
BorderRadius.all(Radius.circular(14)),
),
child: _buildReactions(context),
),
),
_buildReactionsTail(context),
],
),
widthFactor: 0.5,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: rowChildren,
),
),
),
@@ -482,36 +452,6 @@ class _MessageWidgetState extends State<MessageWidget> {
);
}
Widget _buildReactions(BuildContext context) {
final reactionAssets = StreamChatTheme.of(context).reactionAssets;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
...widget.message.reactionCounts.keys.map((reactionType) {
final reactionAsset = reactionAssets.firstWhere(
(reactionAsset) => reactionAsset.type == reactionType,
orElse: () => null,
);
if (reactionAsset == null) {
return Text(
'?',
style: TextStyle(
color: StreamChatTheme.of(context).accentColor,
),
);
}
return SvgPicture.asset(
reactionAsset.svgAsset,
package: reactionAsset.package,
height: 16,
color: StreamChatTheme.of(context).accentColor,
);
}).toList(),
],
);
}
void _showMessageModalBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showDialog(
@@ -825,97 +765,3 @@ class _MessageWidgetState extends State<MessageWidget> {
);
}
}
class ReactionBubblePainter extends CustomPainter {
final Color color;
final Color borderColor;
final int reactionsCount;
ReactionBubblePainter(
this.color,
this.borderColor,
this.reactionsCount,
);
@override
void paint(Canvas canvas, Size size) {
_drawArc(size, canvas);
_drawBorder(size, canvas);
_drawOval(size, canvas);
_drawOvalBorder(size, canvas);
}
void _drawOvalBorder(Size size, Canvas canvas) {
final paint = Paint()
..color = borderColor
..strokeWidth = 1
..style = PaintingStyle.stroke;
final path = Path();
path.addOval(
Rect.fromCircle(
center: Offset(0, 3),
radius: 2,
),
);
canvas.drawPath(path, paint);
}
void _drawOval(Size size, Canvas canvas) {
final paint = Paint()
..color = color
..strokeWidth = 1;
final path = Path();
path.addOval(Rect.fromCircle(
center: Offset(0, 3),
radius: 2,
));
canvas.drawPath(path, paint);
}
void _drawBorder(Size size, Canvas canvas) {
final paint = Paint()
..color = borderColor
..strokeWidth = 1
..style = PaintingStyle.stroke;
final dy = reactionsCount > 1 ? -2.0 : -3.0;
final path = Path();
path.addArc(
Rect.fromCircle(
center: Offset(-4, dy),
radius: 4,
),
-pi * 1.15,
-pi / 1.4,
);
canvas.drawPath(path, paint);
}
void _drawArc(Size size, Canvas canvas) {
final paint = Paint()
..color = color
..strokeWidth = 1;
final dy = reactionsCount > 1 ? -2.0 : -3.0;
final path = Path();
path.addArc(
Rect.fromCircle(
center: Offset(-4, dy),
radius: 4,
),
-pi,
-pi,
);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
+193
View File
@@ -0,0 +1,193 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ReactionBubble extends StatelessWidget {
const ReactionBubble({
Key key,
@required this.reactions,
@required this.borderColor,
@required this.backgroundColor,
this.reverse = false,
this.flipTail = false,
}) : super(key: key);
final List<Reaction> reactions;
final Color borderColor;
final Color backgroundColor;
final bool reverse;
final bool flipTail;
@override
Widget build(BuildContext context) {
final reactionAssets = StreamChatTheme.of(context).reactionAssets;
return Transform(
transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
border: Border.all(
color: borderColor,
),
color: backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(14)),
),
child: Wrap(
children: [
...reactions.map((reaction) {
final reactionAsset = reactionAssets.firstWhere(
(reactionAsset) => reactionAsset.type == reaction.type,
orElse: () => null,
);
if (reactionAsset == null) {
return Text(
'?',
style: TextStyle(
color: StreamChatTheme.of(context).accentColor,
),
);
}
return SvgPicture.asset(
reactionAsset.svgAsset,
package: reactionAsset.package,
height: 16,
color: StreamChatTheme.of(context).accentColor,
);
}).toList(),
],
),
),
_buildReactionsTail(context),
],
),
);
}
Widget _buildReactionsTail(BuildContext context) {
final tail = Transform.translate(
offset: Offset(4, 0),
child: CustomPaint(
painter: ReactionBubblePainter(
backgroundColor,
borderColor,
reactions.length,
),
),
);
if (!flipTail) {
return tail;
} else {
return Transform(
transform: Matrix4.rotationY(pi),
alignment: Alignment.center,
child: tail,
);
}
}
}
class ReactionBubblePainter extends CustomPainter {
final Color color;
final Color borderColor;
final int reactionsCount;
ReactionBubblePainter(
this.color,
this.borderColor,
this.reactionsCount,
);
@override
void paint(Canvas canvas, Size size) {
_drawOval(size, canvas);
_drawOvalBorder(size, canvas);
_drawArc(size, canvas);
_drawBorder(size, canvas);
}
void _drawOvalBorder(Size size, Canvas canvas) {
final paint = Paint()
..color = borderColor
..strokeWidth = 1
..style = PaintingStyle.stroke;
final path = Path();
path.addOval(
Rect.fromCircle(
center: Offset(6, 2),
radius: 2,
),
);
canvas.drawPath(path, paint);
}
void _drawOval(Size size, Canvas canvas) {
final paint = Paint()
..color = color
..strokeWidth = 1;
final path = Path();
path.addOval(Rect.fromCircle(
center: Offset(6, 2),
radius: 2,
));
canvas.drawPath(path, paint);
}
void _drawBorder(Size size, Canvas canvas) {
final paint = Paint()
..color = borderColor
..strokeWidth = 1
..style = PaintingStyle.stroke;
final dy = reactionsCount > 1 ? -2.0 : -3.0;
final startAngle = reactionsCount > 1 ? 1.08 : 1.16;
final sweepAngle = reactionsCount > 1 ? 1.18 : 1.1;
final path = Path();
path.addArc(
Rect.fromCircle(
center: Offset(0, dy),
radius: 4,
),
-pi * startAngle,
-pi / sweepAngle,
);
canvas.drawPath(path, paint);
}
void _drawArc(Size size, Canvas canvas) {
final paint = Paint()
..color = color
..strokeWidth = 1;
final dy = reactionsCount > 1 ? -2.0 : -3.0;
final startAngle = reactionsCount > 1 ? 1.05 : 1.16;
final path = Path();
path.addArc(
Rect.fromCircle(
center: Offset(0, dy),
radius: 4,
),
-pi * startAngle,
-pi,
);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
+3 -3
View File
@@ -25,7 +25,7 @@ class ReactionPicker extends StatelessWidget {
Widget build(BuildContext context) {
final reactionAssets = StreamChatTheme.of(context).reactionAssets;
return Material(
color: messageTheme.reactionsBackgroundColor,
color: messageTheme.ownReactionsBackgroundColor,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
@@ -35,7 +35,7 @@ class ReactionPicker extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: reactionAssets.map((reactionAsset) {
final ownReactionIndex = message.latestReactions?.indexWhere(
final ownReactionIndex = message.ownReactions?.indexWhere(
(reaction) => reaction.type == reactionAsset.type) ??
-1;
return IconButton(
@@ -45,7 +45,7 @@ class ReactionPicker extends StatelessWidget {
package: reactionAsset.package,
color: ownReactionIndex != -1
? StreamChatTheme.of(context).accentColor
: Colors.black,
: Theme.of(context).iconTheme.color,
),
onPressed: () {
if (ownReactionIndex != -1) {
+3 -1
View File
@@ -81,7 +81,9 @@ class StreamChannelState extends State<StreamChannel> {
}
_queryMessageController.add(false);
}).catchError((e, stack) {
_queryMessageController.addError(e, stack);
if (!_queryMessageController.isClosed) {
_queryMessageController.addError(e, stack);
}
});
}
+16
View File
@@ -129,6 +129,14 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
constraints: themeData?.ownMessageTheme?.avatarTheme?.constraints,
borderRadius: themeData?.ownMessageTheme?.avatarTheme?.borderRadius,
),
otherReactionsBorderColor:
themeData?.ownMessageTheme?.otherReactionsBorderColor,
otherReactionsBackgroundColor:
themeData?.ownMessageTheme?.otherReactionsBackgroundColor,
ownReactionsBackgroundColor:
themeData?.ownMessageTheme?.ownReactionsBackgroundColor,
ownReactionsBorderColor:
themeData?.ownMessageTheme?.ownReactionsBorderColor,
),
otherMessageTheme: defaultTheme.otherMessageTheme.copyWith(
replies: themeData?.otherMessageTheme?.replies,
@@ -142,6 +150,14 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
constraints: themeData?.otherMessageTheme?.avatarTheme?.constraints,
borderRadius: themeData?.otherMessageTheme?.avatarTheme?.borderRadius,
),
otherReactionsBorderColor:
themeData?.otherMessageTheme?.otherReactionsBorderColor,
otherReactionsBackgroundColor:
themeData?.otherMessageTheme?.otherReactionsBackgroundColor,
ownReactionsBackgroundColor:
themeData?.otherMessageTheme?.ownReactionsBackgroundColor,
ownReactionsBorderColor:
themeData?.otherMessageTheme?.ownReactionsBorderColor,
),
accentColor: themeData?.accentColor,
secondaryColor: themeData?.secondaryColor,
+31 -14
View File
@@ -302,8 +302,12 @@ class StreamChatThemeData {
fontSize: 12,
),
messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
reactionsBackgroundColor: isDark ? Colors.black : Colors.white,
reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
ownReactionsBackgroundColor:
isDark ? Color(0xff191919) : Color(0xffEAEAEA),
ownReactionsBorderColor: isDark ? Colors.black : Colors.white,
otherReactionsBackgroundColor: isDark ? Colors.black : Colors.white,
otherReactionsBorderColor:
isDark ? Color(0xff191919) : Color(0xffEAEAEA),
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
@@ -316,9 +320,12 @@ class StreamChatThemeData {
),
),
otherMessageTheme: MessageTheme(
reactionsBackgroundColor:
ownReactionsBackgroundColor:
isDark ? Color(0xff191919) : Color(0xffEAEAEA),
ownReactionsBorderColor: isDark ? Colors.black : Colors.white,
otherReactionsBackgroundColor: isDark ? Colors.black : Colors.white,
otherReactionsBorderColor:
isDark ? Color(0xff191919) : Color(0xffEAEAEA),
reactionsBorderColor: theme.scaffoldBackgroundColor,
messageText: TextStyle(
fontSize: 15,
color: isDark ? Colors.white : Colors.black,
@@ -455,8 +462,10 @@ class MessageTheme {
final TextStyle createdAt;
final TextStyle replies;
final Color messageBackgroundColor;
final Color reactionsBackgroundColor;
final Color reactionsBorderColor;
final Color ownReactionsBackgroundColor;
final Color ownReactionsBorderColor;
final Color otherReactionsBackgroundColor;
final Color otherReactionsBorderColor;
final AvatarTheme avatarTheme;
const MessageTheme({
@@ -465,8 +474,10 @@ class MessageTheme {
this.messageAuthor,
this.messageLinks,
this.messageBackgroundColor,
this.reactionsBackgroundColor,
this.reactionsBorderColor,
this.ownReactionsBackgroundColor,
this.ownReactionsBorderColor,
this.otherReactionsBackgroundColor,
this.otherReactionsBorderColor,
this.avatarTheme,
this.createdAt,
});
@@ -478,10 +489,11 @@ class MessageTheme {
TextStyle createdAt,
TextStyle replies,
Color messageBackgroundColor,
Color otherMessageBackgroundColor,
AvatarTheme avatarTheme,
Color reactionsBackgroundColor,
Color reactionsBorderColor,
Color ownReactionsBackgroundColor,
Color ownReactionsBorderColor,
Color otherReactionsBackgroundColor,
Color otherReactionsBorderColor,
}) =>
MessageTheme(
messageText: messageText ?? this.messageText,
@@ -492,9 +504,14 @@ class MessageTheme {
messageBackgroundColor ?? this.messageBackgroundColor,
avatarTheme: avatarTheme ?? this.avatarTheme,
replies: replies ?? this.replies,
reactionsBackgroundColor:
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
ownReactionsBackgroundColor:
ownReactionsBackgroundColor ?? this.ownReactionsBackgroundColor,
ownReactionsBorderColor:
ownReactionsBorderColor ?? this.ownReactionsBorderColor,
otherReactionsBackgroundColor:
otherReactionsBackgroundColor ?? this.otherReactionsBackgroundColor,
otherReactionsBorderColor:
otherReactionsBorderColor ?? this.otherReactionsBorderColor,
);
}
+1 -1
View File
@@ -24,7 +24,7 @@ dependencies:
image_picker: ^0.6.7+2
flutter_keyboard_visibility: ^3.2.1
flutter_svg: ^0.18.0
stream_chat: ^0.2.7+1
stream_chat: ^0.2.8
mime: ^0.9.6+3
visibility_detector: ^0.1.5
line_awesome_icons: ^1.0.4+2