Search code examples
flutterflutter-animation

How to animate Text widget based on TextField's controller value?


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.

This is the desired result

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.


Solution

  • It's a bit tricky, I prepared a widget for you.

    When adding new numbers

    enter image description here

    When deleting numbers

    enter image description here

    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,
            ),
          ),
        );
      }
    }