I have a TextField with its controller and a Text widget, I want to listen to the changes in the TextEditingController and animate the value inside the Text widget.
I want 2 distinguishable animations, one when adding a character and the other when removing a character from the TextField.
I have tried to iterate over each character in the TextEditingController inside a Row widget then render a Text widget containing that value wrapped with a TweenAnimationBuilder and a unique key, the result is working when adding a character only.
It's a bit tricky, I prepared a widget for you.
When adding new numbers
When deleting numbers
Code:
class NumberAnimationSample extends StatefulWidget {
const NumberAnimationSample({super.key});
@override
State<NumberAnimationSample> createState() => _NumberAnimationSampleState();
}
class _NumberAnimationSampleState extends State<NumberAnimationSample> {
String? digits;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: Column(
children: [
Expanded(
child: Container(
color: Colors.blue[800],
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Number',
style: TextStyle(
color: Colors.white,
fontSize: 20,
),
),
AnimatedDigitsWidget(
digits: digits,
),
],
),
),
),
Expanded(
child: Container(
color: Colors.white,
padding: const EdgeInsets.all(8),
child: TextField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(label: Text('Number')),
style: const TextStyle(
fontSize: 25,
),
onChanged: (value) {
setState(
() {
digits = value;
},
);
},
),
),
),
],
),
),
),
);
}
}
class AnimatedDigitsWidget extends StatefulWidget {
const AnimatedDigitsWidget({this.digits, super.key});
final String? digits;
@override
State<AnimatedDigitsWidget> createState() => _AnimatedDigitsWidgetState();
}
class _AnimatedDigitsWidgetState extends State<AnimatedDigitsWidget> {
String currentWidgets = '';
final _animatedDigits = <_MyCustomAnimatedDigit>[];
final _animationControllers = <AnimationController>[];
@override
void didUpdateWidget(covariant AnimatedDigitsWidget oldWidget) {
if (oldWidget.digits != widget.digits) {
currentWidgets = widget.digits ?? '';
if ((oldWidget.digits?.length ?? 0) < currentWidgets.length) {
_animatedDigits.clear();
for (int i = 0; i < currentWidgets.length; i++) {
_animatedDigits.add(
_MyCustomAnimatedDigit(
digit: currentWidgets[i],
onControllerCreated: (controller) {
_animationControllers.add(controller);
if (i == currentWidgets.length - 1) {
controller.forward();
}
},
),
);
}
} else {
var removedDigitsLength =
(oldWidget.digits?.length ?? 0) - currentWidgets.length;
AnimationController? controller;
for (int i = 0; i < removedDigitsLength; i++) {
if (i == 0) {
controller = _animationControllers.removeLast();
} else {
_animationControllers.removeLast();
}
}
controller?.reverse().whenComplete(
() {
setState(
() {
_animatedDigits.clear();
for (int i = 0; i < currentWidgets.length; i++) {
_animatedDigits.add(
_MyCustomAnimatedDigit(
digit: currentWidgets[i],
onControllerCreated: (controller) {},
),
);
}
},
);
},
);
}
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return Row(
children: [
for (final digit in _animatedDigits) digit,
],
);
}
}
class _MyCustomAnimatedDigit extends StatefulWidget {
const _MyCustomAnimatedDigit({
required this.digit,
required this.onControllerCreated,
});
final String digit;
final ValueChanged<AnimationController> onControllerCreated;
@override
State<_MyCustomAnimatedDigit> createState() => __MyCustomAnimatedDigitState();
}
class __MyCustomAnimatedDigitState extends State<_MyCustomAnimatedDigit>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 250),
);
widget.onControllerCreated(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: Curves.easeOut.transform(_controller.value),
child: Transform.rotate(
alignment: Alignment.bottomCenter,
angle: lerpDouble(
-pi / 4, 0, Curves.easeOut.transform(_controller.value))!,
child: child,
),
);
},
child: Text(
widget.digit,
style: const TextStyle(
fontSize: 25,
),
),
);
}
}