Not possible to extend TextField in Flutter?

I'm trying to modify a text field so that I can set different colors to specific words in the same text field. For example "I want an apple" and the word "apple" should be green, rest of the text should be black.

There are libraries for rich text editors (e.g. zefyr, extended_text_field) but I've also found an example of an AnnotatedEditableText here on stackoverflow (https://stackoverflow.com/a/57846261). I like this last solution (AnnotatedEditableText) but I want to use TextField to get richer functionality, primarily text selection that I cannot get to work in a readonly editable text.

Also, when setting expands: true as parameter to TextField the widget correctly expands to fill the area. When setting the same property for an EditableText, nothing happens. Not sure why.

So - I want to use a TextField with the AnnotatedEditableText widget. Can I accomplish this without copy-pasting the entire TextField class? Here's what I've gathered:

  • The _TextFieldState is private and cannot be extended, but the EditableTextState is not private so that widget can be extended.
  • The TextField widget does not support a custom implementation for the EditableText widget.

Any ideas? Why is _TextFieldState private but EditableTextState isn't private?


Solution 1:

with the following controller:

class FruitColorizer extends TextEditingController {
  final Map<String, TextStyle> mapping;
  final Pattern pattern;

  FruitColorizer(this.mapping) : pattern = RegExp(mapping.keys.map((key) => RegExp.escape(key)).join('|'));
  @override
  TextSpan buildTextSpan({TextStyle style, bool withComposing}) {
    List<InlineSpan> children = [];
    // splitMapJoin is a bit tricky here but i found it very handy for populating children list
    text.splitMapJoin(pattern, 
      onMatch: (Match match) {
        children.add(TextSpan(text: match[0], style: style.merge(mapping[match[0]])));
      },
      onNonMatch: (String text) {
        children.add(TextSpan(text: text, style: style));
      },
    );
    return TextSpan(style: style, children: children);
  }
}

use it as:

TextField(
  style: TextStyle(fontSize: 32),
  controller: FruitColorizer({
    'apple': TextStyle(color: Colors.green, decoration: TextDecoration.underline),
    'orange': TextStyle(color: Colors.orange, shadows: kElevationToShadow[2]),
  }),
),

and type in your TextField: "i have eaten two apples, one banana and three oranges"

EDIT

if you want to use just colors you can add the named constructor:

FruitColorizer.fromColors(Map<String, Color> colorMap)
: this(colorMap.map((text, color) => MapEntry(text, TextStyle(color: color))));

and use it like:

controller: FruitColorizer.fromColors({
  'apple': Colors.green,
  'orange': Colors.orange,
}),