【Flutter】RichText でハッシュタグとかテキスト内リンクとか

Flutter には `RichText` という Widget があります。

[RichText class | Flutter](https://api.flutter.dev/flutter/widgets/RichText-class.html)

その名の通り、テキストの一部だけ色やサイズなどを変えられるという、その名の通りリッチな `Text` です。

# RichText の基本

`RichText` の最小限のサンプルコードは以下の通りです。

```dart
RichText(
  text: TextSpan(
    style: TextStyle(color: Colors.black),
    children: [
      TextSpan(
        text: 'これは ',
      ),
      TextSpan(
        text: 'RichText',
        style: TextStyle(color: Colors.blue, fontSize: 24),
      ),
      TextSpan(
        text: ' です。',
      ),
    ],
  ),
),
```

`RichText` の `text` プロパティに `InlineSpan` の子クラス（通常使うのは `TextSpan` かと思います）を指定し、さらに `TextSpan` の `children` プロパティに、スタイルを変更したい場所で文章を区切って `TextSpan` のリストを指定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/4fabb417-7d7f-7a0f-36ea-01436be6f34c.jpeg" width=50%>

なお、`RichText`のデフォルトの文字色は Flutter のデフォルト背景色である黒に合わせて白となっていますので、白背景で消えないように親の `TextSpan` で文字色を黒く指定しています。

「あれ、Flutter の背景色って白じゃない？」という声が聞こえてくる気がしますが、それは `MaterialApp` のデフォルトスタイルです。 `MaterialApp` 背景色を白に、文字色を `DefaultTextStyle` で白に設定するのですが、どうやら `RichText` は `Text` と違ってこの `DefaultTextStyle` を適用しないため、何も指定しないと文字色が白くなってしまう、という理屈だそうです。

__参考：__
[Using the same TextStyle on Text and RichText renders text with different font size ](https://github.com/flutter/flutter/issues/14675#issuecomment-371592999)

ひとまずここまで知っていれば文章中の特定の文字だけスタイルを変えることができるでしょう。

# ハッシュタグとかテキスト内リンクとか

`RichText` は（というか `TextSpan` は）、`recognizer` プロパティでそれぞれの `TextSpan` のタップを検出できるようになっています。

```dart
RichText(
  text: TextSpan(
    style: TextStyle(color: Colors.black),
    children: [
      TextSpan(
        text: 'これは ',
        recognizer: TapGestureRecognizer()..onTap = () {
          print('"これは" がタップされました');
        },
      ),
      TextSpan(
        text: 'RichText',
        style: TextStyle(color: Colors.blue, fontSize: 24),
        recognizer: TapGestureRecognizer()..onTap = () {
          print('"RichText" がタップされました');
        },
      ),
      TextSpan(
        text: ' です。',
        recognizer: TapGestureRecognizer()..onTap = () {
          print('"です" がタップされました');
        },
      ),
    ],
  ),
),

```

これを使うことで、例えばハッシュタグやテキスト内リンクが実現できるようになります。
（なんで `TapGestureRecognizer` への `onTap` への関数の設定がコンストラクタでできないのかがとても気になりますが、そういうもののようです）

これを使うことで、 __テキストの特定部分をタップされたら処理を実行する__ という仕組みを作れるようになります。

つまり、例えば __ハッシュタグ__ や HTML の \<a\> タグのような __リンク__ などの仕組みが実現できる、ということですね。

```dart

RichText(
  text: TextSpan(
    style: TextStyle(color: Colors.black),
    children: [
      TextSpan(
        text: 'これは ',
      ),
      TextSpan(
        text: '#flutter',
        style: TextStyle(color: Colors.blue),
        recognizer: TapGestureRecognizer()..onTap = () {
          // なにか同じハッシュタグの投稿を検索し直す処理
        },
      ),
      TextSpan(
        text: ' についての記事です。\n詳細は ',
      ),
      TextSpan(
        style: TextStyle(color: Colors.blue, decoration: TextDecoration.underline),
        text: 'https://api.flutter.dev/',
        recognizer: TapGestureRecognizer()..onTap = () async {
          await open('https://api.flutter.dev/');
        },
      ),
      TextSpan(
        text: ' を参照してください。',
      ),
    ],
  ),
),
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/659a637e-e1cf-8c1e-5d73-e2e8cc3f46db.png" width=50%>

こんな感じですね。

今回は例として分かりやすくするために文言も分割方法も固定でしたが、例えば Twitter のようにユーザーが自由に投稿した内容から自動でこれらのハッシュタグやリンクを抽出するのであれば、事前に `Matcher` など正規表現を使って文章を分割しておく必要があります。

# 注意点

上の方で書いた `RichText` は `DefaultTextStyle` が適用されないのと同じ話なのか、ユーザーが端末の「アクセシビリティ」設定から文字サイズを変えた場合、 `Text` は勝手に文字が大きくなるのに `RichText` はそのまま、と言う問題が発生します。

以下は同じフォントサイズを指定した、`Text` と `RichText` を縦にならべ、端末の設定から文字サイズを大きくした例です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/251ef2cd-872d-2711-a809-1c8d8eb5c07b.png" width=50%>

この問題は `RichText` の `textScaleFactor` に `MediaQuery.of(context).textScaleFactor` を指定することで解決するようです。（動作確認した限りは解決しました）

```dart
RichText(
  textScaleFactor: MediaQuery.of(context).textScaleFactor,
  text: TextSpan(
    style: TextStyle(color: Colors.black),
    children: [
（以下略）
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134943/1cd8c8d2-71aa-2507-da84-b419706b11fd.png" width=50%>

端末の設定で文字サイズが変わった時に、 `Text` で表示している部分はちゃんと変わってくれたけど `RichText` の部分は変わらない、ということにならないように、この設定も忘れずに指定しておくと良いでしょう。

__参考：__
[widget RichText font size problem ](https://github.com/flutter/flutter/issues/41549#issuecomment-609421030)

#まとめ
個人的な経験では、 `RichText` はここで紹介したハッシュタグやリンクの他、スプレッドシートに__スタイル付き__で入力された元データを同じ見た目でアプリに表示する、という要件でも活躍してくれました。

文字の一部のスタイルを変えたい、文字の一部をタップ可能にしたい、という要件が出てきたらこの `RichText` を検討してみると良いでしょう。