2

I am trying to build a widget that can be shown or hidden, and when it is shown, will have is displayed text 'grow' over a certain duration. I've based it mostly on the typing indicator example.

The idea is to have the widget in e.g. a form with state and provide some pretty user feedback in certain circumstances, such as during and after a validation REST call.

I can't quite figure out how to 'plug in' the AnimationController, in order to grow substring of the input string to display.

i.e. in the form the widget will be something like

AnimatedText(
   textContent: stringFeedback,
   doShowMe: haveFeedback,
            ),

... and in my async input processing method i have a setState(() => haveFeedback = true); and a false etc.

I imagine I need to call a something like the updateText() method below from somewhere somehow linked the the value of the AnimationControler _appearanceController but how to have that be a loop escapes me - still being new to the flutter/Dart and for that matter OOP paradigm.

What I have so far is:

import 'package:flutter/material.dart';
import 'dart:developer' as developer;

class AnimatedText extends StatefulWidget {
  const AnimatedText({
    Key? key,
    this.doShowMe = false,
    this.textContent = '',
  }) : super(key: key);

  final bool doShowMe;
  final String textContent;

  @override
  State<AnimatedText> createState() => _AnimatedTextState();
}

class _AnimatedTextState extends State<AnimatedText>
    with SingleTickerProviderStateMixin {
  late AnimationController _appearanceController;
  late String displayText;

  @override
  void initState() {
    super.initState();
    developer.log('_AnimatedTextState init ');
    _appearanceController = AnimationController(vsync: this);
    displayText = '';
    if (widget.doShowMe) {
      _doShowMe();
    }
  }

  @override
  void didUpdateWidget(AnimatedText oldWidget) {
    super.didUpdateWidget(oldWidget);
    developer.log('_AnimatedTextState didUpdateWidget');
    if (widget.doShowMe != oldWidget.doShowMe) {
      if (widget.doShowMe) {
        developer.log('_AnimatedTextState didUpdateWidget show');
        _doShowMe();
      } else {
        developer.log('_AnimatedTextState didUpdateWidget hide');
        _hideIndicator();
      }
    }
  }

  @override
  void dispose() {
    developer.log('_AnimatedTextState dispose');
    _appearanceController.dispose();
    displayText = '';
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: _appearanceController,
        builder: (context, child) {
          return Container(
            child: Text(displayText),
          );
        });
  }

  void updateText() {
    //something like...
    String payload = widget.textContent;
    if (displayText != payload) {
      int numCharsToShow =
          (_appearanceController.value * widget.textContent.length).ceil();
      displayText = payload.substring(0, numCharsToShow);
      developer.log('updated displayText up to $numCharsToShow');
    }
  }

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

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

Any help much appreciated.

Andre Clements
  • 634
  • 1
  • 7
  • 15
  • A short and incomplete answer to my main question is that there needs to be an [event-listener added](https://api.flutter.dev/flutter/foundation/ChangeNotifier/addListener.html) to the state. Typically in the initState chained to the `AnimationController` so some code can be called each time the value of the animation changes. I'm still wrestling with exactly how to do the plumbing to make the text appear and disappear as expected and will try to post a more comprehensive, and better articulated, answer when I have that resolved. – Andre Clements Jun 06 '22 at 06:48

1 Answers1

0

You can use the addListener method to execute some code whenever the value of the AnimationControler changes.

Between that and the didUpdateWidget method one should be able to deal with most scenarios I think.

In the below code the widget will grow with its payload text appearing as if typed character by character.

When the Boolean variable controling whether it should be shown or hidden changes it will shrink or grow.

If the payload variable changes while the text is being shown, the animation is reset and starts over.

import 'package:flutter/material.dart';

class AnimatedText extends StatefulWidget {
  const AnimatedText({
    Key? key,
    this.doShowMe = false,
    this.textContent = '',
  }) : super(key: key);

  final bool doShowMe;
  final String textContent;

  @override
  State<AnimatedText> createState() => _AnimatedTextState();
}

class _AnimatedTextState extends State<AnimatedText>
    with SingleTickerProviderStateMixin {
  late AnimationController _appearanceController;
  late String displayText;
  late String previousText;

  @override
  void initState() {
    super.initState();
    displayText = '';
    previousText = widget.textContent;
    _appearanceController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1500),
    )..addListener(
        () => updateText(),
      );
    if (widget.doShowMe) {
      _doShowMe();
    }
  }

  void updateText() {
    String payload = widget.textContent;
    int numCharsToShow =
        (_appearanceController.value * widget.textContent.length).ceil();
    if (widget.doShowMe) {
      // make it grow
      displayText = payload.substring(0, numCharsToShow);
    } else {
      // make it shrink
      displayText =
          payload.substring(payload.length - numCharsToShow, payload.length);
    }
  }

  @override
  void didUpdateWidget(AnimatedText oldWidget) {
    super.didUpdateWidget(oldWidget);
    if ((widget.doShowMe != oldWidget.doShowMe) ||
        (widget.textContent != oldWidget.textContent)) {
      if (widget.doShowMe) {
        _doShowMe();
      } else {
        _doHideMe();
      }
    }
    if (widget.doShowMe && widget.textContent != previousText) {
      previousText = widget.textContent;
      _appearanceController
        ..reset()
        ..forward();
    }
  }

  @override
  void dispose() {
    _appearanceController.dispose();
    displayText = '';
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: _appearanceController,
        builder: (context, child) {
          return Text(displayText);
        });
  }

  void _doShowMe() {
    _appearanceController
      ..duration = const Duration(milliseconds: 1500)
      ..forward();
  }

  void _doHideMe() {
    _appearanceController
      ..duration = const Duration(milliseconds: 500)
      ..reverse();
  }
}

Usage example:

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool doShow = true;
  String msg =
      "This is some longer text. Lorem ipsum swhatcha-methingy. "
      "We'll add a simple counter to see what happens when the payload changes. ";
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        //
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Here follows the animated text widget:',
            ),
            AnimatedText(
              doShowMe: doShow,
              textContent: msg,
            ),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  doShow = !doShow;
                });
              },
              child: Text('Toggle showing'),
            ),
            ElevatedButton(
              onPressed: () {
                count++;
                setState(() {
                  msg = '$msg $count';
                });
              },
              child: Text('Change payload'),
            ),
          ],
        ),
      ),
    );
  }
}

Andre Clements
  • 634
  • 1
  • 7
  • 15