Skip to content

Commit

Permalink
fix: 🔨 swipe to reply gesture interaction update
Browse files Browse the repository at this point in the history
  • Loading branch information
vatsaltanna committed Jul 31, 2024
1 parent 2c70e16 commit 2bbce6c
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 149 deletions.
98 changes: 54 additions & 44 deletions lib/src/widgets/chat_bubble_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,7 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
(featureActiveConfig?.enableOtherUserProfileAvatar ?? true))
profileCircle(messagedUser),
Expanded(
child: SwipeToReply(
onLeftSwipe: featureActiveConfig?.enableSwipeToReply ?? true
? isMessageBySender
? onLeftSwipe
: onRightSwipe
: null,
replyIconColor: chatListConfig.swipeToReplyConfig?.replyIconColor,
swipeToReplyAnimationDuration:
chatListConfig.swipeToReplyConfig?.animationDuration,
child: _messagesWidgetColumn(messagedUser),
),
child: _messagesWidgetColumn(messagedUser),
),
if (isMessageBySender) ...[getReceipt()],
if (isMessageBySender &&
Expand Down Expand Up @@ -279,40 +269,60 @@ class _ChatBubbleWidgetState extends State<ChatBubbleWidget> {
onTap: () => widget.onReplyTap
?.call(widget.message.replyMessage.messageId),
),
MessageView(
outgoingChatBubbleConfig:
chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig,
isLongPressEnable:
(featureActiveConfig?.enableReactionPopup ?? true) ||
(featureActiveConfig?.enableReplySnackBar ?? true),
inComingChatBubbleConfig:
chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig,
message: widget.message,
isMessageBySender: isMessageBySender,
messageConfig: chatListConfig.messageConfig,
onLongPress: widget.onLongPress,
chatBubbleMaxWidth: chatListConfig.chatBubbleConfig?.maxWidth,
longPressAnimationDuration:
chatListConfig.chatBubbleConfig?.longPressAnimationDuration,
onDoubleTap: featureActiveConfig?.enableDoubleTapToLike ?? false
? chatListConfig.chatBubbleConfig?.onDoubleTap ??
(message) => currentUser != null
? chatController?.setReaction(
emoji: heart,
messageId: message.id,
userId: currentUser!.id,
)
: null
SwipeToReply(
onLeftSwipe: (featureActiveConfig?.enableSwipeToReply ?? true) &&
isMessageBySender
? onLeftSwipe
: null,
onRightSwipe: (featureActiveConfig?.enableSwipeToReply ?? true) &&
!isMessageBySender
? onRightSwipe
: null,
shouldHighlight: widget.shouldHighlight,
controller: chatController,
highlightColor: chatListConfig.repliedMessageConfig
?.repliedMsgAutoScrollConfig.highlightColor ??
Colors.grey,
highlightScale: chatListConfig.repliedMessageConfig
?.repliedMsgAutoScrollConfig.highlightScale ??
1.1,
onMaxDuration: _onMaxDuration,
replyIconColor: chatListConfig.swipeToReplyConfig?.replyIconColor,
swipeToReplyAnimationDuration:
chatListConfig.swipeToReplyConfig?.animationDuration,
child: Column(
crossAxisAlignment: isMessageBySender
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
MessageView(
outgoingChatBubbleConfig:
chatListConfig.chatBubbleConfig?.outgoingChatBubbleConfig,
isLongPressEnable:
(featureActiveConfig?.enableReactionPopup ?? true) ||
(featureActiveConfig?.enableReplySnackBar ?? true),
inComingChatBubbleConfig:
chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig,
message: widget.message,
isMessageBySender: isMessageBySender,
messageConfig: chatListConfig.messageConfig,
onLongPress: widget.onLongPress,
chatBubbleMaxWidth: chatListConfig.chatBubbleConfig?.maxWidth,
longPressAnimationDuration:
chatListConfig.chatBubbleConfig?.longPressAnimationDuration,
onDoubleTap: featureActiveConfig?.enableDoubleTapToLike ?? false
? chatListConfig.chatBubbleConfig?.onDoubleTap ??
(message) => currentUser != null
? chatController?.setReaction(
emoji: heart,
messageId: message.id,
userId: currentUser!.id,
)
: null
: null,
shouldHighlight: widget.shouldHighlight,
controller: chatController,
highlightColor: chatListConfig.repliedMessageConfig
?.repliedMsgAutoScrollConfig.highlightColor ??
Colors.grey,
highlightScale: chatListConfig.repliedMessageConfig
?.repliedMsgAutoScrollConfig.highlightScale ??
1.1,
onMaxDuration: _onMaxDuration,
),
],
),
),
],
);
Expand Down
34 changes: 16 additions & 18 deletions lib/src/widgets/reply_icon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,11 @@ class ReplyIcon extends StatelessWidget {
const ReplyIcon({
Key? key,
required this.scaleAnimation,
required this.slideAnimation,
this.replyIconColor,
}) : super(key: key);

/// Represents scale animation value of icon when user swipes for reply.
final Animation<double> scaleAnimation;

/// Represents slide animation value of chat bubble when user swipes for reply.
final Animation<Offset> slideAnimation;
final double scaleAnimation;

/// Allow user to set color of icon which is appeared when user swipes for reply.
final Color? replyIconColor;
Expand All @@ -43,26 +39,28 @@ class ReplyIcon extends StatelessWidget {
return Stack(
alignment: Alignment.center,
children: [
Transform.scale(
scale: scaleAnimation.value,
child: CircleAvatar(
radius: 14,
backgroundColor:
scaleAnimation.value == 1.0 ? Colors.grey : Colors.transparent,
child: Icon(
Icons.reply_rounded,
color: replyIconColor ?? Colors.black,
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: scaleAnimation >= 1.0
? Colors.grey.shade300
: Colors.transparent,
),
),
SizedBox(
height: 25,
width: 25,
child: CircularProgressIndicator(
value: scaleAnimation.value,
value: scaleAnimation,
backgroundColor: Colors.transparent,
strokeWidth: 1.5,
color: Colors.grey.shade300,
),
),
Transform.scale(
scale: scaleAnimation,
child: Icon(
Icons.reply_rounded,
color: replyIconColor ?? Colors.black,
size: 20,
),
),
],
Expand Down
157 changes: 70 additions & 87 deletions lib/src/widgets/swipe_to_reply.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* SOFTWARE.
*/
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

import 'reply_icon.dart';

Expand Down Expand Up @@ -55,109 +56,91 @@ class SwipeToReply extends StatefulWidget {
class _SwipeToReplyState extends State<SwipeToReply>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late Animation<double> _leftScaleAnimation;
late Animation<double> _rightScaleAnimation;
late Animation<Offset> _slideAnimation;

@override
void initState() {
super.initState();
_initializeAnimationControllers();
}
double rightPadding = 0;
double tempRightPadding = 0;
double initialTouchPoint = 0;
bool isCallBackTriggered = false;

void _initializeAnimationControllers() {
_controller = AnimationController(
vsync: this,
duration: widget.swipeToReplyAnimationDuration ??
const Duration(milliseconds: 250),
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0.0, 0.0),
end: const Offset(0.0, 0.0),
).animate(CurvedAnimation(curve: Curves.decelerate, parent: _controller));
_leftScaleAnimation = _controller.drive(
Tween<double>(begin: 0.0, end: 0.0),
);
_rightScaleAnimation = _controller.drive(
Tween<double>(begin: 0.0, end: 0.0),
);
}
late bool isMessageBySender = widget.onLeftSwipe == null;

final paddingLimit = 50;

@override
Widget build(BuildContext context) {
final value = rightPadding > 25 ? (rightPadding) / (paddingLimit) : 0.0;
print("value $value");
return GestureDetector(
onHorizontalDragUpdate: _onHorizontalDragUpdate,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
fit: StackFit.passthrough,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Visibility(
visible: widget.onRightSwipe != null,
child: ReplyIcon(
scaleAnimation: _leftScaleAnimation,
slideAnimation: _slideAnimation,
replyIconColor: widget.replyIconColor,
),
),
Visibility(
visible: widget.onLeftSwipe != null,
child: ReplyIcon(
scaleAnimation: _rightScaleAnimation,
slideAnimation: _slideAnimation,
replyIconColor: widget.replyIconColor,
),
),
],
),
SlideTransition(
position: _slideAnimation,
child: widget.child,
),
],
);
},
onHorizontalDragStart: (details) {
initialTouchPoint = details.globalPosition.dx;
},
onHorizontalDragEnd: (details) {
print('onHorizontalDragEnd');
setState(() {
rightPadding = 0;

isCallBackTriggered = false;
});
},
onHorizontalDragUpdate: (details) {
print('onHorizontalDragUpdate');
_onHorizontalDragUpdate(details);
},
child: Stack(
alignment:
isMessageBySender ? Alignment.centerLeft : Alignment.centerRight,
fit: StackFit.passthrough,
children: [
Align(
alignment: widget.onRightSwipe != null ? Alignment.centerLeft : Alignment.centerRight,
child: ReplyIcon(
scaleAnimation: value,
replyIconColor: widget.replyIconColor,
),
),
Padding(
padding: EdgeInsets.only(
right: isMessageBySender ? 0 : rightPadding,
left: isMessageBySender ? rightPadding : 0,
),
child: widget.child,
),
],
),
);
}

void _onHorizontalDragUpdate(DragUpdateDetails details) {
if (widget.onRightSwipe != null && details.delta.dx > 1) {
_runAnimation(onRight: true);
}
if (widget.onLeftSwipe != null && details.delta.dx < -1) {
_runAnimation(onRight: false);
if (!isMessageBySender) {
final swipeDistance = (initialTouchPoint - details.globalPosition.dx);
swipeLogic(swipeDistance, widget.onLeftSwipe);
} else {
print(rightPadding);
final swipeDistance = (details.globalPosition.dx - initialTouchPoint);
swipeLogic(swipeDistance, widget.onRightSwipe);
}
}

void _runAnimation({required bool onRight}) {
_slideAnimation = Tween(
begin: const Offset(0.0, 0.0),
end: Offset(onRight ? 0.1 : -0.1, 0.0),
).animate(CurvedAnimation(curve: Curves.decelerate, parent: _controller));
if (onRight) {
_leftScaleAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(curve: Curves.decelerate, parent: _controller));
void swipeLogic(double swipeDistance, VoidCallback? onSwipe) {
print(rightPadding);
print(paddingLimit);
if (swipeDistance >= 0 && tempRightPadding < paddingLimit) {
setState(() {
rightPadding = swipeDistance;
});
} else if (rightPadding >= paddingLimit) {
print('callback');
if (!isCallBackTriggered && onSwipe != null) {
onSwipe();
isCallBackTriggered = true;
}
} else {
_rightScaleAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(curve: Curves.decelerate, parent: _controller));
}
_controller.forward().whenComplete(() {
_controller.reverse().whenComplete(() {
if (onRight) {
_leftScaleAnimation = _controller.drive(Tween(begin: 0.0, end: 0.0));
if (widget.onRightSwipe != null) widget.onRightSwipe!();
} else {
_rightScaleAnimation = _controller.drive(Tween(begin: 0.0, end: 0.0));
if (widget.onLeftSwipe != null) widget.onLeftSwipe!();
}
setState(() {
rightPadding = 0;
});
});
}

tempRightPadding = swipeDistance;
}

@override
Expand Down

0 comments on commit 2bbce6c

Please sign in to comment.