Skip to content

Commit

Permalink
refactor typing indicator
Browse files Browse the repository at this point in the history
  • Loading branch information
Dat-TG committed Dec 29, 2023
1 parent bca9567 commit c56640d
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 295 deletions.
339 changes: 46 additions & 293 deletions lib/core/widgets/typing_indicator.dart
Original file line number Diff line number Diff line change
@@ -1,331 +1,84 @@
import 'dart:math';
import 'package:flutter/material.dart';

class TypingIndicator extends StatefulWidget {
const TypingIndicator({
super.key,
this.showIndicator = true,
this.bubbleColor = const Color(0xFF646b7f),
this.flashingCircleDarkColor = const Color(0xFF333333),
this.flashingCircleBrightColor = const Color(0xFFaec1dd),
});

final bool showIndicator;
final Color bubbleColor;
final Color flashingCircleDarkColor;
final Color flashingCircleBrightColor;

const TypingIndicator({Key? key}) : super(key: key);
@override
State<TypingIndicator> createState() => _TypingIndicatorState();
_TypingIndicatorState createState() => _TypingIndicatorState();
}

class _TypingIndicatorState extends State<TypingIndicator>
with TickerProviderStateMixin {
late AnimationController _appearanceController;

late Animation<double> _indicatorSpaceAnimation;

late Animation<double> _smallBubbleAnimation;
late Animation<double> _mediumBubbleAnimation;
late Animation<double> _largeBubbleAnimation;

late AnimationController _repeatingController;
final List<Interval> _dotIntervals = const [
Interval(0.25, 0.8),
Interval(0.35, 0.9),
Interval(0.45, 1.0),
];
with SingleTickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
super.initState();

_appearanceController = AnimationController(
vsync: this,
)..addListener(() {
setState(() {});
});

_indicatorSpaceAnimation = CurvedAnimation(
parent: _appearanceController,
curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut),
).drive(Tween<double>(
begin: 0.0,
end: 60.0,
));

_smallBubbleAnimation = CurvedAnimation(
parent: _appearanceController,
curve: const Interval(0.0, 0.5, curve: Curves.elasticOut),
reverseCurve: const Interval(0.0, 0.3, curve: Curves.easeOut),
);
_mediumBubbleAnimation = CurvedAnimation(
parent: _appearanceController,
curve: const Interval(0.2, 0.7, curve: Curves.elasticOut),
reverseCurve: const Interval(0.2, 0.6, curve: Curves.easeOut),
);
_largeBubbleAnimation = CurvedAnimation(
parent: _appearanceController,
curve: const Interval(0.3, 1.0, curve: Curves.elasticOut),
reverseCurve: const Interval(0.5, 1.0, curve: Curves.easeOut),
);

_repeatingController = AnimationController(
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);

if (widget.showIndicator) {
_showIndicator();
}
}

@override
void didUpdateWidget(TypingIndicator oldWidget) {
super.didUpdateWidget(oldWidget);

if (widget.showIndicator != oldWidget.showIndicator) {
if (widget.showIndicator) {
_showIndicator();
} else {
_hideIndicator();
}
}
duration: Duration(milliseconds: 800),
)..repeat();
}

@override
void dispose() {
_appearanceController.dispose();
_repeatingController.dispose();
_controller.dispose();
super.dispose();
}

void _showIndicator() {
_appearanceController
..duration = const Duration(milliseconds: 750)
..forward();
_repeatingController.repeat();
}

void _hideIndicator() {
_appearanceController
..duration = const Duration(milliseconds: 150)
..reverse();
_repeatingController.stop();
}

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _indicatorSpaceAnimation,
builder: (context, child) {
return SizedBox(
height: _indicatorSpaceAnimation.value,
child: child,
);
},
return Padding(
padding: const EdgeInsets.only(
left: 14,
right: 56,
),
child: Stack(
children: [
AnimatedBubble(
animation: _smallBubbleAnimation,
left: 12,
bottom: 12,
bubble: StatusBubble(
repeatingController: _repeatingController,
dotIntervals: _dotIntervals,
flashingCircleDarkColor: widget.flashingCircleDarkColor,
flashingCircleBrightColor: widget.flashingCircleBrightColor,
bubbleColor: widget.bubbleColor,
Container(
width: 100,
padding:
const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.grey.shade300,
),
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.min,
children: [
_buildDot(1),
_buildDot(2),
_buildDot(3),
],
),
),
),
],
),
);
}
}

class CircleBubble extends StatelessWidget {
const CircleBubble({
super.key,
required this.size,
required this.bubbleColor,
});

final double size;
final Color bubbleColor;

@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: bubbleColor,
),
);
}
}

class AnimatedBubble extends StatelessWidget {
const AnimatedBubble({
super.key,
required this.animation,
required this.left,
required this.bottom,
required this.bubble,
});

final Animation<double> animation;
final double left;
final double bottom;
final Widget bubble;

@override
Widget build(BuildContext context) {
return Positioned(
left: left,
bottom: bottom,
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.scale(
scale: animation.value,
alignment: Alignment.bottomLeft,
child: child,
);
},
child: bubble,
),
);
}
}

class StatusBubble extends StatelessWidget {
const StatusBubble({
super.key,
required this.repeatingController,
required this.dotIntervals,
required this.flashingCircleBrightColor,
required this.flashingCircleDarkColor,
required this.bubbleColor,
});

final AnimationController repeatingController;
final List<Interval> dotIntervals;
final Color flashingCircleDarkColor;
final Color flashingCircleBrightColor;
final Color bubbleColor;

@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: 14,
right: 56,
top: 10,
bottom: 10,
Widget _buildDot(int dotNumber) {
return ScaleTransition(
scale: Tween<double>(begin: 1.0, end: 0.5).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
(dotNumber - 1) * 0.2, // Adjust the interval for each dot
dotNumber * 0.2,
curve: Curves.easeInOut,
),
),
),
child: Container(
width: 100,
width: 10.0,
height: 10.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.grey.shade300,
),
padding:
const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FlashingCircle(
index: 0,
repeatingController: repeatingController,
dotIntervals: dotIntervals,
flashingCircleDarkColor: flashingCircleDarkColor,
flashingCircleBrightColor: flashingCircleBrightColor,
),
FlashingCircle(
index: 1,
repeatingController: repeatingController,
dotIntervals: dotIntervals,
flashingCircleDarkColor: flashingCircleDarkColor,
flashingCircleBrightColor: flashingCircleBrightColor,
),
FlashingCircle(
index: 2,
repeatingController: repeatingController,
dotIntervals: dotIntervals,
flashingCircleDarkColor: flashingCircleDarkColor,
flashingCircleBrightColor: flashingCircleBrightColor,
),
],
color: Colors.grey,
shape: BoxShape.circle,
),
),
);
}
}

class FlashingCircle extends StatelessWidget {
const FlashingCircle({
super.key,
required this.index,
required this.repeatingController,
required this.dotIntervals,
required this.flashingCircleBrightColor,
required this.flashingCircleDarkColor,
});

final int index;
final AnimationController repeatingController;
final List<Interval> dotIntervals;
final Color flashingCircleDarkColor;
final Color flashingCircleBrightColor;

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: repeatingController,
builder: (context, child) {
final circleFlashPercent = dotIntervals[index].transform(
repeatingController.value,
);
final circleColorPercent = sin(pi * circleFlashPercent);

return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color.lerp(
flashingCircleDarkColor,
flashingCircleBrightColor,
circleColorPercent,
),
),
);
},
);
}
}

class FakeMessage extends StatelessWidget {
const FakeMessage({
super.key,
required this.isBig,
});

final bool isBig;

@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
height: isBig ? 128 : 36,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Colors.grey.shade300,
),
);
}
}
2 changes: 1 addition & 1 deletion lib/presentation/chat_screen/chat_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class _ChatScreenState extends State<ChatScreen> {
index ==
_chatStore
.chatThreads[chatThreadIndex].messages.length) {
return TypingIndicator();
return const TypingIndicator();
}
return Container(
key: ValueKey(
Expand Down
2 changes: 1 addition & 1 deletion lib/presentation/new_chat/new_chat_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
],
),
),
const TypingIndicator(),
TypingIndicator(),
],
),
),
Expand Down

0 comments on commit c56640d

Please sign in to comment.