Merge pull request #144 from GetStream/feature/animations

Feature/animations
This commit is contained in:
Salvatore Giordano
2020-11-23 12:20:07 +01:00
committed by GitHub
7 changed files with 447 additions and 293 deletions
+29 -6
View File
@@ -95,7 +95,13 @@ class MessageActionsModal extends StatelessWidget {
messageTheme: messageTheme, messageTheme: messageTheme,
), ),
), ),
IgnorePointer( TweenAnimationBuilder<double>(
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( child: MessageWidget(
key: Key('MessageWidget'), key: Key('MessageWidget'),
reverse: reverse, reverse: reverse,
@@ -116,10 +122,22 @@ class MessageActionsModal extends StatelessWidget {
shape: messageShape, shape: messageShape,
), ),
), ),
);
}),
SizedBox( SizedBox(
height: 8, height: 8,
), ),
Padding( TweenAnimationBuilder<double>(
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( padding: const EdgeInsets.symmetric(
horizontal: 48.0, horizontal: 48.0,
), ),
@@ -129,7 +147,8 @@ class MessageActionsModal extends StatelessWidget {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment:
CrossAxisAlignment.stretch,
children: ListTile.divideTiles( children: ListTile.divideTiles(
context: context, context: context,
tiles: [ tiles: [
@@ -139,15 +158,19 @@ class MessageActionsModal extends StatelessWidget {
message.status == null) && message.status == null) &&
message.parentId == null) message.parentId == null)
_buildReplyButton(context), _buildReplyButton(context),
if (showEditMessage) _buildEditMessage(context), if (showEditMessage)
_buildEditMessage(context),
if (showDeleteMessage) if (showDeleteMessage)
_buildDeleteButton(context), _buildDeleteButton(context),
if (showCopyMessage) _buildCopyButton(context), if (showCopyMessage)
_buildCopyButton(context),
], ],
).toList(), ).toList(),
), ),
), ),
) ),
);
})
], ],
), ),
), ),
+38 -10
View File
@@ -540,6 +540,14 @@ class MessageInputState extends State<MessageInput> {
bottom: size.height + MediaQuery.of(context).viewInsets.bottom, bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0, left: 0,
right: 0, right: 0,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300),
curve: Curves.easeInOutExpo,
builder: (context, val, wid) {
return Transform.scale(
alignment: Alignment.center,
scale: val,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Card( child: Card(
@@ -560,15 +568,17 @@ class MessageInputState extends State<MessageInput> {
children: [ children: [
if (commands.isNotEmpty) if (commands.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(left: 8.0, top: 8.0), padding:
const EdgeInsets.only(left: 8.0, top: 8.0),
child: Row( child: Row(
children: [ children: [
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 8.0), horizontal: 8.0),
child: Icon( child: Icon(
StreamIcons.lightning, StreamIcons.lightning,
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context)
.accentColor,
), ),
), ),
Text( Text(
@@ -583,11 +593,14 @@ class MessageInputState extends State<MessageInput> {
...commands ...commands
.map( .map(
(c) => ListTile( (c) => ListTile(
leading: c.name == 'giphy' ? _buildGiphyIcon() : null, leading: c.name == 'giphy'
? _buildGiphyIcon()
: null,
title: Text.rich( title: Text.rich(
TextSpan( TextSpan(
text: '${c.name.capitalize()}', text: '${c.name.capitalize()}',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(
fontWeight: FontWeight.bold),
children: [ children: [
TextSpan( TextSpan(
text: ' /${c.name} ${c.args}', text: ' /${c.name} ${c.args}',
@@ -621,6 +634,8 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
); );
}),
);
}); });
} }
@@ -929,6 +944,13 @@ class MessageInputState extends State<MessageInput> {
bottom: size.height + MediaQuery.of(context).viewInsets.bottom, bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0, left: 0,
right: 0, right: 0,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300),
curve: Curves.easeInOutExpo,
builder: (context, val, wid) {
return Transform.scale(
scale: val,
child: Card( child: Card(
margin: EdgeInsets.all(8.0), margin: EdgeInsets.all(8.0),
elevation: 2.0, elevation: 2.0,
@@ -962,12 +984,14 @@ class MessageInputState extends State<MessageInput> {
), ),
title: Text( title: Text(
'${m.user.name}', '${m.user.name}',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(
fontWeight: FontWeight.bold),
), ),
subtitle: Text('@${m.userId}'), subtitle: Text('@${m.userId}'),
trailing: Icon( trailing: Icon(
StreamIcons.at_mention, StreamIcons.at_mention,
color: StreamChatTheme.of(context).accentColor, color: StreamChatTheme.of(context)
.accentColor,
), ),
onTap: () { onTap: () {
_mentionedUsers.add(m.user); _mentionedUsers.add(m.user);
@@ -975,9 +999,11 @@ class MessageInputState extends State<MessageInput> {
splits[splits.length - 1] = m.user.name; splits[splits.length - 1] = m.user.name;
final rejoin = splits.join('@'); final rejoin = splits.join('@');
textEditingController.value = TextEditingValue( textEditingController.value =
TextEditingValue(
text: rejoin + text: rejoin +
textEditingController.text.substring( textEditingController.text
.substring(
textEditingController textEditingController
.selection.start), .selection.start),
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
@@ -995,6 +1021,8 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
); );
}),
);
}); });
} }
+21 -3
View File
@@ -86,7 +86,13 @@ class MessageReactionsModal extends StatelessWidget {
messageTheme: messageTheme, messageTheme: messageTheme,
), ),
), ),
IgnorePointer( TweenAnimationBuilder<double>(
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( child: MessageWidget(
key: Key('MessageWidget'), key: Key('MessageWidget'),
reverse: reverse, reverse: reverse,
@@ -107,6 +113,8 @@ class MessageReactionsModal extends StatelessWidget {
showReactionPickerIndicator: true, showReactionPickerIndicator: true,
), ),
), ),
);
}),
SizedBox( SizedBox(
height: 16, height: 16,
), ),
@@ -178,7 +186,14 @@ class MessageReactionsModal extends StatelessWidget {
BuildContext context, BuildContext context,
) { ) {
final isCurrentUser = reaction.user.id == currentUser.id; final isCurrentUser = reaction.user.id == currentUser.id;
return ConstrainedBox( return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
builder: (context, val, snapshot) {
return Transform.scale(
scale: val,
child: ConstrainedBox(
constraints: BoxConstraints.loose(Size( constraints: BoxConstraints.loose(Size(
64, 64,
98, 98,
@@ -205,7 +220,8 @@ class MessageReactionsModal extends StatelessWidget {
child: ReactionBubble( child: ReactionBubble(
reactions: [reaction], reactions: [reaction],
borderColor: messageTheme.reactionsBorderColor, borderColor: messageTheme.reactionsBorderColor,
backgroundColor: messageTheme.reactionsBackgroundColor, backgroundColor:
messageTheme.reactionsBackgroundColor,
highlightOwnReactions: false, highlightOwnReactions: false,
), ),
), ),
@@ -222,6 +238,8 @@ class MessageReactionsModal extends StatelessWidget {
), ),
], ],
), ),
),
); );
});
} }
} }
+91 -12
View File
@@ -1,4 +1,6 @@
import 'package:ezanimation/ezanimation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
@@ -8,7 +10,8 @@ import '../stream_chat_flutter.dart';
/// It shows a reaction picker /// It shows a reaction picker
/// ///
/// Usually you don't use this widget as it's one of the default widgets used by [MessageWidget.onMessageActions]. /// 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({ const ReactionPicker({
Key key, Key key,
@required this.message, @required this.message,
@@ -18,11 +21,44 @@ class ReactionPicker extends StatelessWidget {
final Message message; final Message message;
final MessageTheme messageTheme; final MessageTheme messageTheme;
@override
_ReactionPickerState createState() => _ReactionPickerState();
}
class _ReactionPickerState extends State<ReactionPicker>
with TickerProviderStateMixin {
List<EzAnimation> animations = [];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final reactionIcons = StreamChatTheme.of(context).reactionIcons; final reactionIcons = StreamChatTheme.of(context).reactionIcons;
return Material(
color: messageTheme.reactionsBackgroundColor, 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),
vsync: this,
),
);
});
triggerAnimations();
}
return TweenAnimationBuilder<double>(
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, clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
@@ -32,22 +68,39 @@ class ReactionPicker extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: reactionIcons.map((reactionIcon) { children: reactionIcons.map((reactionIcon) {
final ownReactionIndex = message.ownReactions?.indexWhere( final ownReactionIndex = widget.message.ownReactions
(reaction) => reaction.type == reactionIcon.type) ?? ?.indexWhere((reaction) =>
reaction.type == reactionIcon.type) ??
-1; -1;
var index = reactionIcons.indexOf(reactionIcon);
return IconButton( return IconButton(
iconSize: 24, iconSize: 24,
icon: Icon( 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, reactionIcon.iconData,
size: animations[index].value * 24.0,
color: ownReactionIndex != -1 color: ownReactionIndex != -1
? StreamChatTheme.of(context).accentColor ? StreamChatTheme.of(context).accentColor
: Theme.of(context).iconTheme.color.withOpacity(.5), : Theme.of(context)
.iconTheme
.color
.withOpacity(.5),
), ),
);
}),
onPressed: () { onPressed: () {
if (ownReactionIndex != -1) { if (ownReactionIndex != -1) {
removeReaction( removeReaction(
context, context,
message.ownReactions[ownReactionIndex], widget.message.ownReactions[ownReactionIndex],
); );
} else { } else {
sendReaction( sendReaction(
@@ -59,18 +112,44 @@ class ReactionPicker extends StatelessWidget {
); );
}).toList(), }).toList(),
), ),
),
); );
});
}
void triggerAnimations() async {
for (var a in animations) {
a.start();
await Future.delayed(Duration(milliseconds: 100));
}
}
void pop() async {
for (var a in animations) {
a.stop();
}
Navigator.of(context).pop();
} }
/// Add a reaction to the message /// Add a reaction to the message
void sendReaction(BuildContext context, String reactionType) { void sendReaction(BuildContext context, String reactionType) {
StreamChannel.of(context).channel.sendReaction(message, reactionType); StreamChannel.of(context)
Navigator.of(context).pop(); .channel
.sendReaction(widget.message, reactionType);
pop();
} }
/// Remove a reaction from the message /// Remove a reaction from the message
void removeReaction(BuildContext context, Reaction reaction) { 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(); pop();
}
@override
void dispose() {
for (var a in animations) {
a?.dispose();
}
super.dispose();
} }
} }
+1
View File
@@ -40,6 +40,7 @@ dependencies:
git: https://github.com/imtoori/media_gallery.git git: https://github.com/imtoori/media_gallery.git
permission_handler: ^5.0.1+1 permission_handler: ^5.0.1+1
transparent_image: ^1.0.0 transparent_image: ^1.0.0
ezanimation: ^0.4.1
flutter: flutter:
assets: assets:
+2
View File
@@ -40,6 +40,7 @@ void main() {
); );
await tester.pump(); await tester.pump();
await tester.pump(Duration(milliseconds: 1000));
expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.byKey(Key('MessageWidget')), findsOneWidget);
expect(find.byIcon(StreamIcons.sorting_up), findsOneWidget); expect(find.byIcon(StreamIcons.sorting_up), findsOneWidget);
expect(find.byIcon(StreamIcons.edit), findsOneWidget); expect(find.byIcon(StreamIcons.edit), findsOneWidget);
@@ -84,6 +85,7 @@ void main() {
); );
await tester.pump(); await tester.pump();
await tester.pump(Duration(milliseconds: 1000));
expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.byKey(Key('MessageWidget')), findsOneWidget);
expect(find.byIcon(StreamIcons.sorting_up), findsNothing); expect(find.byIcon(StreamIcons.sorting_up), findsNothing);
expect(find.byIcon(StreamIcons.edit), findsNothing); expect(find.byIcon(StreamIcons.edit), findsNothing);
@@ -39,6 +39,8 @@ void main() {
), ),
); );
await tester.pump(Duration(milliseconds: 1000));
expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.byKey(Key('MessageWidget')), findsOneWidget);
expect(find.byIcon(StreamIcons.thumbs_up_reaction), findsOneWidget); expect(find.byIcon(StreamIcons.thumbs_up_reaction), findsOneWidget);
}, },
@@ -87,6 +89,7 @@ void main() {
); );
await tester.pump(); await tester.pump();
await tester.pump(Duration(milliseconds: 1000));
expect(find.byKey(Key('MessageWidget')), findsOneWidget); expect(find.byKey(Key('MessageWidget')), findsOneWidget);
expect(find.byIcon(StreamIcons.thumbs_up_reaction), findsNWidgets(2)); expect(find.byIcon(StreamIcons.thumbs_up_reaction), findsNWidgets(2));
expect(find.byIcon(StreamIcons.love_reaction), findsNWidgets(2)); expect(find.byIcon(StreamIcons.love_reaction), findsNWidgets(2));