diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 16c48e25..049f1be7 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -95,58 +95,78 @@ class MessageActionsModal extends StatelessWidget { messageTheme: messageTheme, ), ), - IgnorePointer( - child: MessageWidget( - key: Key('MessageWidget'), - reverse: reverse, - message: message.copyWith( - text: message.text.length > 200 - ? '${message.text.substring(0, 200)}...' - : message.text, - ), - messageTheme: messageTheme, - showReactions: false, - showUsername: false, - showReplyIndicator: false, - showUserAvatar: showUserAvatar, - showTimestamp: false, - translateUserAvatar: false, - showReactionPickerIndicator: true, - showSendingIndicator: DisplayWidget.gone, - shape: messageShape, - ), + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300), + builder: (context, val, snapshot) { + return Transform.scale( + scale: val, + child: IgnorePointer( + child: MessageWidget( + key: Key('MessageWidget'), + reverse: reverse, + message: message.copyWith( + text: message.text.length > 200 + ? '${message.text.substring(0, 200)}...' + : message.text, + ), + messageTheme: messageTheme, + showReactions: false, + showUsername: false, + showReplyIndicator: false, + showUserAvatar: showUserAvatar, + showTimestamp: false, + translateUserAvatar: false, + showReactionPickerIndicator: true, + showSendingIndicator: DisplayWidget.gone, + shape: messageShape, + ), + ), + ); + } ), SizedBox( height: 8, ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 48.0, - ), - child: Material( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: ListTile.divideTiles( - context: context, - tiles: [ - if (showReply && - (message.status == - MessageSendingStatus.SENT || - message.status == null) && - message.parentId == null) - _buildReplyButton(context), - if (showEditMessage) _buildEditMessage(context), - if (showDeleteMessage) - _buildDeleteButton(context), - if (showCopyMessage) _buildCopyButton(context), - ], - ).toList(), - ), - ), + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + builder: (context, val, wid) { + return Transform( + transform: Matrix4.identity()..scale(val)..rotateZ(-1.0 + val), + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 48.0, + ), + child: Material( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: ListTile.divideTiles( + context: context, + tiles: [ + if (showReply && + (message.status == + MessageSendingStatus.SENT || + message.status == null) && + message.parentId == null) + _buildReplyButton(context), + if (showEditMessage) _buildEditMessage(context), + if (showDeleteMessage) + _buildDeleteButton(context), + if (showCopyMessage) _buildCopyButton(context), + ], + ).toList(), + ), + ), + ), + ); + } ) ], ), diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index e7d302e6..1946e9ff 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -1,4 +1,6 @@ +import 'package:ezanimation/ezanimation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; import '../stream_chat_flutter.dart'; @@ -8,7 +10,8 @@ import '../stream_chat_flutter.dart'; /// It shows a reaction picker /// /// Usually you don't use this widget as it's one of the default widgets used by [MessageWidget.onMessageActions]. -class ReactionPicker extends StatelessWidget { + +class ReactionPicker extends StatefulWidget { const ReactionPicker({ Key key, @required this.message, @@ -18,59 +21,115 @@ class ReactionPicker extends StatelessWidget { final Message message; final MessageTheme messageTheme; + @override + _ReactionPickerState createState() => _ReactionPickerState(); +} + +class _ReactionPickerState extends State { + List animations = []; + @override Widget build(BuildContext context) { final reactionIcons = StreamChatTheme.of(context).reactionIcons; - return Material( - color: messageTheme.reactionsBackgroundColor, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: reactionIcons.map((reactionIcon) { - final ownReactionIndex = message.ownReactions?.indexWhere( - (reaction) => reaction.type == reactionIcon.type) ?? - -1; - return IconButton( - iconSize: 24, - icon: Icon( - reactionIcon.iconData, - color: ownReactionIndex != -1 - ? StreamChatTheme.of(context).accentColor - : Theme.of(context).iconTheme.color.withOpacity(.5), + + if (animations.isEmpty && reactionIcons.isNotEmpty) { + reactionIcons.forEach((element) { + animations.add( + EzAnimation.sequence([ + SequenceItem(0.0, 1.4), + SequenceItem(1.4, 1.0), + ], Duration(milliseconds: 500)), + ); + }); + + triggerAnimations(); + } + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + curve: Curves.easeInOutExpo, + duration: Duration(milliseconds: 500), + builder: (context, val, wid) { + return Transform.scale( + scale: val, + child: Material( + color: widget.messageTheme.reactionsBackgroundColor, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: reactionIcons.map((reactionIcon) { + final ownReactionIndex = widget.message.ownReactions + ?.indexWhere((reaction) => + reaction.type == reactionIcon.type) ?? + -1; + var index = reactionIcons.indexOf(reactionIcon); + + return IconButton( + iconSize: 24, + icon: AnimatedBuilder( + animation: animations[index], + builder: (context, val) { + return Transform( + transform: Matrix4.identity() + ..scale(animations[index].value, + animations[index].value) + ..rotateZ(1.0 - animations[index].value), + child: Icon( + reactionIcon.iconData, + size: animations[index].value * 24.0, + color: ownReactionIndex != -1 + ? StreamChatTheme.of(context).accentColor + : Theme.of(context) + .iconTheme + .color + .withOpacity(.5), + ), + ); + }), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + widget.message.ownReactions[ownReactionIndex], + ); + } else { + sendReaction( + context, + reactionIcon.type, + ); + } + }, + ); + }).toList(), + ), ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, - message.ownReactions[ownReactionIndex], - ); - } else { - sendReaction( - context, - reactionIcon.type, - ); - } - }, ); - }).toList(), - ), - ); + }); + } + + void triggerAnimations() async { + for (var a in animations) { + a.start(); + await Future.delayed(Duration(milliseconds: 100)); + } } /// Add a reaction to the message void sendReaction(BuildContext context, String reactionType) { - StreamChannel.of(context).channel.sendReaction(message, reactionType); + StreamChannel.of(context) + .channel + .sendReaction(widget.message, reactionType); Navigator.of(context).pop(); } /// Remove a reaction from the message void removeReaction(BuildContext context, Reaction reaction) { - StreamChannel.of(context).channel.deleteReaction(message, reaction); + StreamChannel.of(context).channel.deleteReaction(widget.message, reaction); Navigator.of(context).pop(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 1716bab1..689a54b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: media_gallery: ^0.1.5 permission_handler: ^5.0.1+1 transparent_image: ^1.0.0 + ezanimation: ^0.4.0 flutter: assets: