I am trying to build a list view with cards with different animation behaviors depending on user interactions like hovering. The hovered card needs to be scaled up and the remaining cards in the list need to be scaled down and made less opaque.
Like:
Expected smoothness
View code:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:simple_animations/multi_tween/multi_tween.dart';
import 'package:simple_animations/simple_animations.dart';
import 'package:simple_animations/stateless_animation/custom_animation.dart';
import '../controllers/work_controller.dart';
class WorkView extends GetView<WorkController> {
#override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
flex: 1,
child: Container(color: Colors.blueGrey),
),
Expanded(
flex: 2,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
10,
(index) {
return MouseRegion(
onExit: (event) => controller.hoverIndex.value = -1,
onEnter: ((event) {
print("Setting index: $index");
controller.hoverIndex.value = index;
}),
child: AnimatedWorkCard(
index: index,
));
},
),
),
))
],
);
}
}
class AnimatedWorkCard extends StatefulWidget {
final int index;
const AnimatedWorkCard({
Key? key,
required this.index,
}) : super(key: key);
#override
State<AnimatedWorkCard> createState() => _AnimatedWorkCardState();
}
enum CardAnimationProps {
isNotHoveredOpacity,
isHoveredImgScale,
isNotHoveredTranslateX,
isHoveredTextTranslateY,
isHoveredTextOpacity,
}
class _AnimatedWorkCardState extends State<AnimatedWorkCard> {
final WorkController controller = Get.find();
var animationControl = CustomAnimationControl.play;
#override
Widget build(BuildContext context) {
return Obx(() {
var isHovered = controller.hoverIndex.value == widget.index;
if (!isHovered) {
return CustomAnimation<TimelineValue<CardAnimationProps>>(
control: animationControl,
tween: createNotHoveredTween(),
builder: (context, child, value) {
return Transform.translate(
offset: Offset(
value.get(CardAnimationProps.isNotHoveredTranslateX), 0),
child: child,
);
},
child: Card(
color: Colors.amber,
child: Container(
width: Get.width * 0.2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Work ${widget.index}',
style: TextStyle(
fontSize: Get.width * 0.03,
),
),
ElevatedButton(
child: Text('Hovering: ${controller.hoverIndex.value}'),
onPressed: () {
Get.toNamed('/work/${widget.index}');
},
),
],
),
),
),
);
} else {
return CustomAnimation<TimelineValue<CardAnimationProps>>(
control: animationControl,
tween: createTween(controller.hoverIndex.value, widget.index),
builder: (context, child, value) {
return Transform.scale(
scale: value.get(CardAnimationProps.isHoveredImgScale),
child: child ?? Container());
},
child: Card(
color: Colors.amber,
child: Container(
width: Get.width * 0.2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Work ${widget.index}',
style: TextStyle(
fontSize: Get.width * 0.03,
),
),
ElevatedButton(
child: Text('Hovering: ${controller.hoverIndex.value}'),
onPressed: () {
Get.toNamed('/work/${widget.index}');
},
),
],
),
),
),
);
}
});
}
createTween(int hoverIndex, int index) {
TimelineTween timelineTween = TimelineTween<CardAnimationProps>();
var scene =
timelineTween.addScene(begin: 0.seconds, end: 2000.milliseconds);
scene.animate(
CardAnimationProps.isHoveredImgScale,
tween: Tween<double>(begin: 1, end: 1.3),
);
scene.animate(
CardAnimationProps.isHoveredTextOpacity,
tween: Tween<double>(begin: 0, end: 1),
);
scene.animate(
CardAnimationProps.isHoveredTextTranslateY,
tween: Tween<double>(begin: 20, end: 0),
);
return timelineTween;
}
createNotHoveredTween() {
TimelineTween timelineTween = TimelineTween<CardAnimationProps>();
var scene =
timelineTween.addScene(begin: 0.seconds, end: 2000.milliseconds);
scene.animate(
CardAnimationProps.isNotHoveredOpacity,
tween: Tween<double>(begin: 1, end: 0.2),
);
scene.animate(
CardAnimationProps.isNotHoveredTranslateX,
tween: Tween<double>(
begin: 0,
end: 20,
),
);
return timelineTween;
}
}
Controller code:
import 'package:get/get.dart';
class WorkController extends GetxController {
RxInt hoverIndex = (-1).obs;
#override
void onReady() {
super.onReady();
}
#override
void onClose() {}
}
But the animation is not smooth and it's just jumping from the states.
Any idea how this can made smoother or any other way this can be thought of implementing?
Animation demo
Related
I m trying to create the following view on my app, other area are done but now comes to the core feature of the app, which allows people to record the audio and stack other audio on top of the one that has been recorded, before going on the hard parts of recording and margin or trim the audios, I am stuck on the view, plz anyone who can shade a light on this will be appreciated. spare the bottom navigation bar, that one has no issue, only the timeline board.
here the view that I just prototyped.
Here some code that I've tried to play with but failed.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class Studio extends StatefulWidget {
const Studio({Key? key}) : super(key: key);
#override
_Studio createState() => _Studio();
}
class _Studio extends State<Studio> with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _time = 0.0, _scale = 1.0;
int _minutes = 0;
int _seconds = 0;
#override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 60));
_controller.addListener(() {
setState(() {
_time = _controller.value;
_minutes = (_time * 60).floor();
_seconds = ((_time * 60) % 1 * 60).floor();
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Timeline'),
),
body: Column(
children: <Widget>[
Expanded(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 12,
itemBuilder: (context, index) {
return Container(
width: 50,
height: 50,
margin: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text('$index'),
),
);
},
),
),
Container(
padding: EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('$_minutes'),
Text(':'),
Text('$_seconds'),
],
),
),
ElevatedButton(
onPressed: () {
if (_controller.isAnimating) {
_controller.stop();
} else {
_controller.forward();
}
},
child: Text(_controller.isAnimating ? 'Stop' : 'Start'),
),
],
),
);
}
void _onScaleStart(ScaleStartDetails details) {
print(details);
setState(() {
//_scale = details.focalPoint;
});
}
void _onScaleUpdate(ScaleUpdateDetails details) {
setState(() {
_scale = details.scale;
});
}
Widget _buildTimeline() {
return Container(
height: 40,
child: Row(
children: <Widget>[
_buildTimelineMinute(0),
_buildTimelineMinute(5),
_buildTimelineMinute(10),
],
),
);
}
Widget _buildTimelineHour(int hour) {
return Container(
width: 10,
color: Colors.green,
child: Center(
child: Text(
"$hour:00",
style: TextStyle(color: Colors.black, fontSize: 12),
),
),
);
}
Widget _buildTimelineMinute(int minute) {
return Container(
width: 10,
color: Colors.green,
child: Center(
child: Text(
"$minute",
style: TextStyle(color: Colors.black, fontSize: 12),
),
),
);
}
}
Thank you
I have 2 button, to control a scrolling of InteractiveViewer widget, left and right, hopefully I can add up and down later own my own after i have better understanding of matrix4Tween
right now the button is as such
Row(
children: [
ElevatedButton(
onPressed: () {
controller.value = Matrix4.identity()..translate(0.0, 0.0);
},
child: Text('<'),
),
ElevatedButton(
onPressed: () {
controller.value = Matrix4.identity()..translate(-(width), 0.0);
},
child: Text('>'),
),
],
),
and below it is a interactiveViewer widget that contains 4 gridview
Container(
color: Colors.grey,
width: gridboxwidth,
height: gridboxheight,
child: InteractiveViewer(
alignPanAxis: true,
constrained: false,
transformationController: controller,
scaleEnabled: true,
minScale: 0.1,
maxScale: 1,
child: Column(
children: [
Row(
children: [
grid1(size),
grid3(size),
],
),
Row(
children: [
grid2(size),
grid4(size),
],
),
],
),
),
),
and it works just fine, on tap of the button, the grid moves into view, but I don't think it is intuitive enough and some animation might need to be added to show that the grid has changed.
Any help and guidance is greatly appreciated.
this is a sample widget that uses Matrix4Tween, the important lines of code are marked with // NOTE: comments:
class FooInteractiveViewer extends StatefulWidget {
#override
_FooInteractiveViewerState createState() => _FooInteractiveViewerState();
}
class _FooInteractiveViewerState extends State<FooInteractiveViewer> with TickerProviderStateMixin {
AnimationController _ctrl;
Animation<Matrix4> _matrixAnimation = AlwaysStoppedAnimation(Matrix4.identity());
final _transformationController = TransformationController();
#override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
)
// NOTE: add listener to be called each time _ctrl changes
..addListener(_listener);
}
void _listener() {
print(MatrixUtils.transformPoint(_matrixAnimation.value, Offset.zero));
// NOTE: this is the most important part of this code:
_transformationController.value = _matrixAnimation.value;
}
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return InteractiveViewer(
// panEnabled: false,
alignPanAxis: true,
constrained: false,
transformationController: _transformationController,
child: SizedBox(
width: constraints.maxWidth * 2,
height: constraints.maxHeight * 2,
child: Column(
children: [
Expanded(
child: Row(
children: [
button(Colors.red, constraints.biggest, 1, 0, 'green'),
button(Colors.green, constraints.biggest, 0, 1, 'blue'),
],
),
),
Expanded(
child: Row(
children: [
button(Colors.blue, constraints.biggest, 1, 1, 'orange'),
button(Colors.orange, constraints.biggest, 0, 0, 'red'),
],
),
),
],
),
)
);
},
);
}
Widget button(Color color, Size size, int x, int y, String name) {
return Expanded(
child: Material(
color: color,
child: InkWell(
onTap: () async {
// timeDilation = 10;
// NOTE: create new Animation<Matrix4> that will be used inside _listener
_matrixAnimation = Matrix4Tween(
begin: _transformationController.value,
end: Matrix4.translationValues(-size.width * x, -size.height * y, 0)
).chain(CurveTween(curve: Curves.decelerate)).animate(_ctrl);
// NOTE: lets start the show
await _ctrl.forward(from: 0);
print('### animation finished ###');
},
child: Center(child: Text('go to $name', textScaleFactor: 2)),
),
),
);
}
#override
void dispose() {
super.dispose();
_ctrl.dispose();
}
}
I want to slide out my first widget from right and slide in second from left of screen.
I'm trying to use AnimatedSwitcher with SlideTransition
my current code bug is that first widget doesn't slide out and just vanishes
here is my complete code snippet.
Any help would be appriciated
class LoginPage extends StatefulWidget {
LoginPage({Key? key}) : super(key: key);
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage>
with SingleTickerProviderStateMixin {
static const int PIN_CODE_LENGTH = 4;
final TextEditingController _mobileController = TextEditingController();
final TextEditingController _pinController = TextEditingController();
final UniqueKey _mobileKey = UniqueKey();
final UniqueKey _pinKey = UniqueKey();
bool _submittable = false;
bool _isLoginStepOne = true;
String _buttonText = Strings.next;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Directionality(
textDirection: TextDirection.rtl,
child: SingleChildScrollView(
child: SizedBox(
height: SizePercentConfig.screenHeight,
child: Column(
children: [
_buildHeader(),
Expanded(
child: _buildForm(),
),
],
),
),
),
),
);
}
Widget _buildHeader() {
return Container(
height: SizePercentConfig.safeBlockVertical * 60,
child: Stack(
children: [
Positioned(
bottom: 0,
right: SizePercentConfig.blockSizeHorizontal * 30,
left: SizePercentConfig.blockSizeHorizontal * 30,
child: Image.asset(
Assets.logo,
fit: BoxFit.fitWidth,
),
),
Container(
height: SizePercentConfig.safeBlockVertical * 50,
child: Stack(
children: [
Positioned(
bottom: 0,
child: Image.asset(
Assets.loginHeader,
width: SizePercentConfig.screenWidth,
fit: BoxFit.fitWidth,
),
),
],
),
),
],
),
);
}
Widget _buildForm() {
return Form(
onChanged: _validate,
child: Padding(
padding: const EdgeInsets.all(Dimens.unitX2),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(seconds: 1),
transitionBuilder: (Widget child, Animation<double> animation) {
final inAnimation = Tween<Offset>(
begin: Offset(1.0, 0.0), end: Offset(0.0, 0.0))
.animate(animation);
final outAnimation = Tween<Offset>(
begin: Offset(-1.0, 0.0), end: Offset(0.0, 0.0))
.animate(animation);
print('** child key: ${child.key}');
print('** mobile key: $_mobileKey');
print('** pin key: $_pinKey');
if (child.key == _mobileKey) {
// in animation
print('>>>>>>> first statement');
return ClipRect(
child: SlideTransition(
position: inAnimation,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: child,
),
),
);
} else {
// out animation
print('>>>>>>> second statement');
return ClipRect(
child: SlideTransition(
position: outAnimation,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: child,
),
),
);
}
},
layoutBuilder:
(Widget? currentChild, List<Widget> previousChildren) {
return currentChild!;
},
child: _isLoginStepOne
? AppTextField(
key: _mobileKey,
controller: _mobileController,
hint: Strings.mobileNumber,
textInputType: TextInputType.phone,
)
: _buildPinCode()),
SizedBox(height: Dimens.unitX2),
AppSolidButton(
onPressed: _buttonAction,
text: _buttonText,
width: SizePercentConfig.screenWidth,
enabled: _submittable,
),
SizedBox(height: Dimens.unitX2),
],
),
),
);
}
void _validate() {
if (_isLoginStepOne) {
if (Regex.mobileRegex.hasMatch(_mobileController.value.text) !=
_submittable)
setState(() {
print('--> setState called in _validate');
_submittable = !_submittable;
});
} else {
if ((_pinController.value.text.length == 4) != _submittable)
setState(() {
print('--> setState called in _validate');
_submittable = !_submittable;
});
}
}
void _buttonAction() {
if (_submittable) {
setState(() {
print('--> setState called in _buttonPressed');
_isLoginStepOne = false;
_submittable = false;
_buttonText = Strings.login;
});
} else {}
}
Widget _buildPinCode() {
return Directionality(
textDirection: TextDirection.ltr,
child: PinCodeTextField(
key: _pinKey,
controller: _pinController,
appContext: context,
length: PIN_CODE_LENGTH,
onChanged: (_) {},
enablePinAutofill: true,
enableActiveFill: true,
textStyle: TextStyle(color: Palette.scorpion),
pinTheme: PinTheme(
shape: PinCodeFieldShape.circle,
fieldHeight: SizePercentConfig.safeBlockHorizontal * 20,
fieldWidth: SizePercentConfig.safeBlockHorizontal * 20,
activeFillColor: Palette.concrete,
inactiveFillColor: Palette.concrete,
selectedFillColor: Palette.roseBud,
activeColor: Palette.concrete,
disabledColor: Palette.concrete,
inactiveColor: Palette.concrete,
selectedColor: Palette.roseBud,
),
cursorColor: Palette.transparent,
keyboardType: TextInputType.number,
),
);
}
}
Give your ClipRect widgets unique keys:
If the "new" child is the same widget type and key as the "old" child, but with different parameters, then AnimatedSwitcher will not do a transition between them, since as far as the framework is concerned, they are the same widget and the existing widget can be updated with the new parameters. To force the transition to occur, set a Key on each child widget that you wish to be considered unique (typically a ValueKey on the widget data that distinguishes this child from the others).
I want to make a flipping animation with TweenAnimationBuilder, a container will flip over and change the color. I want to add a button when user click on it, the container will flip over again and change into another colour.
Here is my code:
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool isBack = true;
Color backColor = Colors.green;
Color topColor = Colors.red;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
box(),
SizedBox(height: 10),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size(25, 10),
elevation: 10,
),
onPressed: () {
setState(() {
backColor = Colors.red;
topColor = Colors.blue;
});
},
child:
Text('change to blue', style: TextStyle(fontSize: 16))),
],
),
),
),
);
}
Widget box() {
print('building');
return TweenAnimationBuilder(
tween: Tween<double>(begin: 0, end: pi),
duration: Duration(seconds: 1),
builder: (BuildContext context, double value, _) {
print(value);
if (value >= (pi / 2)) {
isBack = false;
} else {
isBack = true;
}
return (Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(value),
child: Container(
width: 100,
height: 100,
child: isBack
? Container(
color: backColor,
)
: Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateY(
pi),
child: Container(
color: topColor,
),
)
),
));
});
}
}
At the first build, the tween value will start from the beginning:
building
0
0
0
0.13080335172486462
0.19619246121668257
0.2180893620122034
...
3.141592653589793
but when I click on the button to change the color, it will not start again from the begin value, it just stays at 3.14:
building
3.141592653589793
Right now the button will only change the color of the container, but it will not flip again.
I suppose after the setstate function, the tween value will restart again at 0, why won't it do so?
Can anybody explain it please?
All you need is an AnimationController and AnimatedBuilder to control the animation.
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
bool isBack = true;
Color backColor = Colors.blue;
Color topColor = Colors.red;
late AnimationController _animationController;
late Animation _rotationAnimation;
#override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
_rotationAnimation =
Tween<double>(begin: 0, end: pi).animate(_animationController);
_animationController.forward();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
box(),
SizedBox(height: 10),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size(25, 10),
elevation: 10,
),
onPressed: () {
setState(() {
backColor = Colors.red;
topColor = Colors.blue;
});
// if (_animationController.isDismissed) {
// _animationController.forward();
// } else if (_animationController.isCompleted) {
// _animationController.reverse();
// }
_animationController.forward(from: 0.0);
},
child:
Text('change to blue', style: TextStyle(fontSize: 16))),
],
),
),
),
);
}
Widget box() {
return AnimatedBuilder(
animation: _animationController,
// tween: Tween<double>(begin: 0, end: pi),
// duration: Duration(seconds: 1),
builder: (_, __) {
// print(value);
if (_rotationAnimation.value >= (pi / 2)) {
isBack = false;
} else {
isBack = true;
}
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(_rotationAnimation.value),
child: Container(
width: 100,
height: 100,
child: isBack
? Container(
color: backColor,
)
: Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..rotateY(pi),
child: Container(
color: topColor,
),
),
),
);
});
}
}
i am developing a chatbot and i want to add a typing indicator before the bot reply to the user, i have tried switching between widgets after the user inputs a message using future delayed, but it is not working at all.
the following code demonstrates how i tried using the future delayed:
bool _nextWidget = false;
#override
void initState() {
super.initState();
}
void myMethod() {
Future.delayed(
const Duration(
seconds: 5,
milliseconds: 500,
),
() {
if (this.mounted) {
setState(() {
_nextWidget = true;
});
}
});
}
Widget bot(String message) {
myMethod();
return _nextWidget ? botMessage(message) : botInd();
}
Widget botInd() {
return Container(
alignment: Alignment.bottomLeft,
margin: EdgeInsets.only(top: 20),
child: Container(
constraints: BoxConstraints(maxWidth: 75, maxHeight: 100),
child: JumpingDotsProgressIndicator(fontSize: 50.0, color: Colors.white)
)
);
}
Widget botMessage(String message) {
return ChatBubble(
clipper: ChatBubbleClipper2(type: BubbleType.receiverBubble),
alignment: Alignment.bottomLeft,
margin: EdgeInsets.only(top: 20),
backGroundColor: Colors.white,
child: Container(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.7),
child: Text(
message,
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold)
)
)
);
}
Have a look at https://flutter.dev/docs/cookbook/effects/typing-indicator it explains all necessary steps how to create a typing indicator with code examples.
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(
const MaterialApp(
home: ExampleIsTyping(),
debugShowCheckedModeBanner: false,
),
);
}
const _backgroundColor = Color(0xFF333333);
class ExampleIsTyping extends StatefulWidget {
const ExampleIsTyping({
Key? key,
}) : super(key: key);
#override
_ExampleIsTypingState createState() => _ExampleIsTypingState();
}
class _ExampleIsTypingState extends State<ExampleIsTyping> {
bool _isSomeoneTyping = false;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _backgroundColor,
appBar: AppBar(
title: const Text('Typing Indicator'),
),
body: Column(
children: [
Expanded(
child: _buildMessages(),
),
Align(
alignment: Alignment.bottomLeft,
child: TypingIndicator(
showIndicator: _isSomeoneTyping,
),
),
_buildIsTypingSimulator(),
],
),
);
}
Widget _buildMessages() {
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8.0),
itemCount: 25,
reverse: true,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(left: 100.0),
child: FakeMessage(isBig: index.isOdd),
);
},
);
}
Widget _buildIsTypingSimulator() {
return Container(
color: Colors.grey,
padding: const EdgeInsets.all(16),
child: Center(
child: CupertinoSwitch(
onChanged: (newValue) {
setState(() {
_isSomeoneTyping = newValue;
});
},
value: _isSomeoneTyping,
),
),
);
}
}
class TypingIndicator extends StatefulWidget {
const TypingIndicator({
Key? key,
this.showIndicator = false,
this.bubbleColor = const Color(0xFF646b7f),
this.flashingCircleDarkColor = const Color(0xFF333333),
this.flashingCircleBrightColor = const Color(0xFFaec1dd),
}) : super(key: key);
final bool showIndicator;
final Color bubbleColor;
final Color flashingCircleDarkColor;
final Color flashingCircleBrightColor;
#override
_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),
];
#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(
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();
}
}
}
#override
void dispose() {
_appearanceController.dispose();
_repeatingController.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,
);
},
child: Stack(
children: [
_buildAnimatedBubble(
animation: _smallBubbleAnimation,
left: 8,
bottom: 8,
bubble: _buildCircleBubble(8),
),
_buildAnimatedBubble(
animation: _mediumBubbleAnimation,
left: 10,
bottom: 10,
bubble: _buildCircleBubble(16),
),
_buildAnimatedBubble(
animation: _largeBubbleAnimation,
left: 12,
bottom: 12,
bubble: _buildStatusBubble(),
),
],
),
);
}
Widget _buildAnimatedBubble({
required Animation<double> animation,
required double left,
required double bottom,
required Widget bubble,
}) {
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,
),
);
}
Widget _buildCircleBubble(double size) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.bubbleColor,
),
);
}
Widget _buildStatusBubble() {
return Container(
width: 85,
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(27),
color: widget.bubbleColor,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFlashingCircle(0),
_buildFlashingCircle(1),
_buildFlashingCircle(2),
],
),
);
}
Widget _buildFlashingCircle(int index) {
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(widget.flashingCircleDarkColor,
widget.flashingCircleBrightColor, circleColorPercent),
),
);
},
);
}
}
#immutable
class FakeMessage extends StatelessWidget {
const FakeMessage({
Key? key,
required this.isBig,
}) : super(key: key);
final bool isBig;
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
height: isBig ? 128.0 : 36.0,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: Colors.grey.shade300,
),
);
}
}