Seyit Gokce
Seyit Gokce

Reputation: 363

Finding Sum of 'length's of 'text's in TextSpans in Flutter

Information

I needed to apply typewriting animation to the texts on a website I was building with Flutter. I have successfully implemented this animation. For the animation to be applied, the value of the 'length' and 'style' properties of the text to be written to the screen and taken as a parameter must be known. Here is the widget I wrote:

// @dart=2.9
import 'package:flutter/material.dart';

// ignore: must_be_immutable
class TypeWriterAnimatedText extends StatefulWidget {
  TextSpan textSpan;
  TypeWriterAnimatedText({Key key, @required this.textSpan}) : super(key: key);

  @override
  _TypeWriterAnimatedTextState createState() => _TypeWriterAnimatedTextState();
}

class _TypeWriterAnimatedTextState extends State<TypeWriterAnimatedText> with TickerProviderStateMixin {
  Animation<double> _textBlinkCursorAnimation;
  Animation<double> _textBlinkCursorAnimationCurve;
  AnimationController _textBlinkCursorAnimationController;
  
  bool _blink = true;


  @override
  void initState() {
    super.initState();
    _textBlinkCursorAnimationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 200));
    _textBlinkCursorAnimationCurve = CurvedAnimation(
        parent: _textBlinkCursorAnimationController,
        curve: Curves.easeInOutQuart,
        reverseCurve: Curves.linear);
    _textBlinkCursorAnimation = Tween<double>(begin: 0, end: 1)
        .animate(_textBlinkCursorAnimationCurve)
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed && _blink == true) {
          _textBlinkCursorAnimationController.reverse();
        } else if (status == AnimationStatus.dismissed && _blink == true) {
          _textBlinkCursorAnimationController.forward();
        }
      });
    _textBlinkCursorAnimationController.forward();

  }
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100,
      child: TweenAnimationBuilder<int>(
        tween: IntTween(begin: 0, end: widget.textSpan.text.length),
        builder: (BuildContext context, int value, Widget child){
          WidgetsBinding.instance.addPostFrameCallback((_){
            if(value == widget.textSpan.text.length){
              Future.delayed(Duration(milliseconds: 1800)).then((value){
                setState(() {
                  _blink = false;
                  _textBlinkCursorAnimationController.reset();
                });
              });
            }
          });
          return SelectableText.rich(
            TextSpan(
                children: [
                  widget.textSpan,
                  TextSpan(
                    text: _blink == false ? '' : '_',
                    style: widget.textSpan.style.copyWith(color: widget.textSpan.style.color.withOpacity(_textBlinkCursorAnimation.value)),
                  ),
                ]),
          );
        },
        duration: Duration(seconds: 2),
      ),
    );
  }
}


The Problem

But the widget I wrote to apply the animation needs to take TextSpan as a parameter in the constructor. Here, I come across a problem. TextSpans are in a nested structure. On the other hand, I need to find the 'length' sum of all 'text' properties, and 'style' of last TextSpan's 'text' property in the TextSpan that I take as a parameter. But since TextSpans are nested, some of them return null for the 'length' and 'style' getters (widget.textSpan.text.length and widget.textSpan.style). This is causing me to get an error.

What I Want To Do?

Even if the data I give to the Widget as a TextSpan contains nested TextSpans, I need to access the 'length' properties of all 'text's and find the sum of it. For example, I need to find the sum of the lengths of the 'Text1-1', 'Text1-2' and 'Text2' Strings, even if there is a data like here:

TextSpan(
    children: [
        TextSpan(
          children: [
        TextSpan(
          text: 'Text1-1',
          style: TextStyle(
              fontSize: 40,
              fontFamily: 'RobotoMono-Bold',
              color: Colors.white),
            ),
        TextSpan(
          text: 'Text1-2',
          style: TextStyle(
              fontSize: 16,
              fontFamily: 'RobotoMono-Bold',
              color: Colors.white),
            ),
          ],
          style: TextStyle(
              fontSize: 40,
              fontFamily: 'RobotoMono-Bold',
              color: Colors.white),
            ),
        TextSpan(
          text: 'Text2',
          style: TextStyle(
              fontSize: 16,
              fontFamily: 'RobotoMono-Bold',
              color: Colors.white),
            ),
          ],
        ),

Upvotes: 2

Views: 248

Answers (1)

cameron1024
cameron1024

Reputation: 10136

If you have a nested TextSpan, you can call toPlainText to render the full text into a String:

final span = TextSpan(
  children: [
    TextSpan(text: 'hello'),
    TextSpan(text: 'world'),
  ],
);
span.toPlainText()  // returns 'helloworld'

Then you can call length on that String:

int totalLength(TextSpan span) => span.toPlainText().length;

For the style, you can use the visitor pattern, which Flutter uses extensively. You can call textSpan.visitChildren(visitor) to walk the full tree and "visit" each child recursively.

For example, if you want to get the last style in the tree, you could use:

TextStyle finalStyle(TextSpan span) {

  TextStyle? style;

  bool visit(InlineSpan span) {
    if (span.style != null) style = span.style;  // if you find a style, keep a reference to it
    return true;  // continue through the tree
  }

  span.visitChildren(visit);

  return style ?? (throw AssertionError('no styles found'));
}

You could also return a TextStyle? instead of throwing if none are found, depends on your use case.

Upvotes: 2

Related Questions