【Flutter】入力テキスト中のURL部分の色を動的に変える

# はじめに
入力中のテキスト内のURL部分について、入力に従って動的に色（テキストスタイル）を変える方法を紹介します。
テキスト中のURL部分の判別方法については、以下の記事を参考にしてください。

https://qiita.com/jimborie/items/22e675ef52fe6246b913


 # 目標
 入力中のテキスト内のURL部分のテキストスタイルを動的に変更する。

 # 実装

テキストを入力するための`TextFormFieald`ウィジェットのソースコードは次のとおりです。

https://github.com/flutter/flutter/blob/4d9e56e694/packages/flutter/lib/src/material/text_form_field.dart#L88

`TextFormFieald`には `TextEditingController`が設定できます。
サンプルコードは以下です。
`TextEditingController`は入力テキストに対するコントローラになります。

```dart:
TextFormField(
  controller: _textEditingController, //TextEditingControllerのインスタンス
  //その他のプロパティ
)
```

ここで、`TextEditingController`の内部実装を確認すると次の通りです。

https://github.com/flutter/flutter/blob/4d9e56e694/packages/flutter/lib/src/widgets/editable_text.dart#L126

キーボードから入力したテキストがUI部分に反映されるのは、以下`buildTextSpan`メソッドの部分です。

```dart:buildTextSpan
TextSpan buildTextSpan({required BuildContext context, TextStyle? style , required bool withComposing}) {
  assert(!value.composing.isValid || !withComposing || value.isComposingRangeValid);
  // If the composing range is out of range for the current text, ignore it to
  // preserve the tree integrity, otherwise in release mode a RangeError will
  // be thrown and this EditableText will be built with a broken subtree.
  final bool composingRegionOutOfRange = !value.isComposingRangeValid || !withComposing;

  if (composingRegionOutOfRange) {
    return TextSpan(style: style, text: text);
  }

  final TextStyle composingStyle = style?.merge(const TextStyle(decoration: TextDecoration.underline))
      ?? const TextStyle(decoration: TextDecoration.underline);
  return TextSpan(
    style: style,
    children: <TextSpan>[
      TextSpan(text: value.composing.textBefore(value.text)),
      TextSpan(
        style: composingStyle,
        text: value.composing.textInside(value.text),
      ),
      TextSpan(text: value.composing.textAfter(value.text)),
    ],
  );
}
```

UI部分に表示するべき入力ずみテキストを`TextSpan`の形で返却していることがわかります。

したがって入力したテキストの表示に何らかの変更を加えたい場合は、`TextEditingController`クラスを継承したカスタムクラスを作成し、`buildTextSpan`メソッドをオーバライドすれば良いことがわかります。

ここでは`CustomTextEditingController`クラスを以下のように作成します。

また与えたテキストからURL部分を判別し、リンクを設定する`AutoLinkText`クラスを既に別途定義ずみと仮定します（詳細は前回記事参照）。

```dart:
class CustomTextEdittingController extends TextEditingController {
  CustomTextEdittingController({String? text}) : super(text: text);

  @override
TextSpan buildTextSpan(
  {required BuildContext context,
    TextStyle? style,
    required bool withComposing}) {
  assert(!value.composing.isValid ||
      !withComposing ||
      value.isComposingRangeValid);

  return AutoLinkText.generate( // URL部分のみ別のスタイルを適用したTextSpanを返却するクラス
    this.text,　//入力したテキスト
    hasLink: false, //リンク機能の有無のフラグ
  ).generate();
  }
}
```

この`CustomTextEdittingController`を使用して、次のように`TextFormField`を使用すると、入力中のテキストの正規表現によって判別されるURL部分のテキストスタイルを任意のもので表示することが可能です。
例えばURL部分のみ青文字で表示するといった仕様が想定されます。

ただしこのままでは、変換等が未確定の文字（`buildTextSpan`中の`value.composing.textInside`で取得できる範囲の文字列）につく下線が表示されないので、未確定文字に対して下線をつけて`TextSpan`を返却するよう修正します。

未確定文字に対して下線をつけたくない場合はここまでのコーディングで問題ありません。

前述した通り、未確定文字は`buildTextSpan`中の`value.composing.textInside`から取得することができます。

ここで改めて`TextEditingController`の`buildTextSpan`のコードを参照すると、

```dart:
return TextSpan(
  style: style,
  children: <TextSpan>[
    TextSpan(text: value.composing.textBefore(value.text)),
    TextSpan(
      style: composingStyle,
      text: value.composing.textInside(value.text),
    ),
    TextSpan(text: value.composing.textAfter(value.text)),
  ],
);
```

このように`textInside`で取得される入力が未確定な文字列の前後に分かれて、それらの文字列が`textBefore`および`textAfter`として取得されて、それぞれの部分ごとに`TextSpan`が生成されていることがわかります。

生成された`TextSpan`を最終的に一つの`TextSpan`としてUIに表示する仕組みです。

したがって入力が未確定の文字列に対して下線をつけるためには次のように`CustomTextEdittingController`に変更を加えます。


```dart:
class CustomTextEdittingController extends TextEditingController {
  CustomTextEdittingController({String? text}) : super(text: text);

  @override
  TextSpan buildTextSpan(
    {required BuildContext context,
    TextStyle? style,
    required bool withComposing}) {
  assert(!value.composing.isValid ||
      !withComposing ||
      value.isComposingRangeValid);

    //入力確定済みのテキストに対しての処理
    if (!value.isComposingRangeValid || !withComposing) {
      return AutoLinkTextSpan.generate(
        this.text,
        hasLink: false,
        hasUnderline: false,
      ).generate();
    }

    //入力中のテキストに対する処理
    return TextSpan(
      children: <TextSpan>[
        TextSpan(
          children: [
            AutoLinkTextSpan.generate(
              value.composing.textBefore(value.text),
              hasLink: false,
              hasUnderline: false,
            ).generate()
          ],
        ),
        TextSpan(
          children: [
            AutoLinkTextSpan.generate(
              value.composing.textInside(value.text),
              hasLink: false,
              hasUnderline: true, //下線を加えるフラグ（AutoLinkTextSpan内でTextStyle変更）
            ).generate()
          ],
        ),
        TextSpan(
          children: [
            AutoLinkTextSpan.generate(
              value.composing.textAfter(value.text),
              hasLink: false,
              hasUnderline: false,
            ).generate()
          ],
        ),
      ],
    );
  }
}
```

以上で実装した`CustomTextEditingController`を`TextFormField`のコントローラとして指定すると、本記事の目標である「入力中のテキスト内のURL部分のテキストスタイルを動的に変更する」が達成できます。


# おわりに
入力中のテキスト内のURL部分のテキストスタイルを動的に変更する方法を紹介しました。
URL部分の判別および自動でテキストスタイルや処理をセットする方法については前回記事に詳しく解説しております。
あわせてご覧ください。