-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
48 additions
and
295 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters