コミュニティ

[Flutter] キーワードと一致する文字列をハイライトする方法

この記事は最終更新日から1年以上が経過しています。

はじめに

検索機能を実装した際、キーワードとの一致箇所をハイライトする機能がほしくなったので方法を探してみたのですが見つからなかったので自力で実装しました。

Screenshot_1572937804.png

デモ用にアプリを作成してコードを掲載していますのでコピペして試してみてください。

おことわり

今回紹介する方法ではスペースを含む文字列は正常にハイライトされません。

考えられる一致のパターン

  • 一致する箇所なし
    pattern0.png

  • 文字列の先頭で一致
    pattern1.png

  • 文字列の途中で一致
    pattern2.png

  • 文字列の末尾で一致
    pattern3.png

  • 文字列全体が一致
    pattern4.png

つまり、文字列は最少で1つ、最多で3つに分割されます。
Textウィジェットを3つ持つ配列を返すような関数にしてやればよさそうです。

コード

ハイライトの処理

List<Widget> getHighlightedText(String originalString, String inputString) {
  // 大文字/小文字の区別をなくすため、すべて小文字に変換
  final String lowerOriginalString = originalString.toLowerCase();
  final String lowerInputString = inputString.toLowerCase();
  // もとの文字列における入力された文字列の最初の文字のインデックス
  final int firstOfInputString = lowerOriginalString.indexOf(lowerInputString);
  // もとの文字列における入力された文字列の最後の文字のインデックス
  final int lastOfInputString =
        lowerOriginalString.indexOf(lowerInputString) +
            (lowerInputString.length - 1);
  final double _fontSize = 24.0;
  final Color _highlightColor = Colors.blue;


  // inputStringと一致する箇所がない場合のエラー(Value not in range: -1)回避
  if (firstOfInputString == -1) {
    return [Container()];
  }

  final List<Widget> highlightedText = [
    Text(
      originalString.substring(0, firstOfInputString),
      style: TextStyle(
        fontSize: _fontSize,
      ),
    ),
    Text(
      originalString.substring(firstOfInputString, lastOfInputString + 1),
      style: TextStyle(
        color: _highlightColor,
        fontSize: _fontSize,
      ),
    ),
    Text(
      originalString.substring(lastOfInputString + 1),
      style: TextStyle(
        fontSize: _fontSize,
      ),
    ),
  ];
  return highlightedText;
}

アプリ全体

main.dart
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Highlight Text Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Highlight Text Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  String originalText = '';
  String inputText = '';
  String highlightedText = '';
  final TextEditingController _originalTextController = TextEditingController();
  final TextEditingController _highlightedPartController =
      TextEditingController();

  void refreshScreen() {
    setState(() {
      originalText = '';
      inputText = '';
      highlightedText = '';
      _originalTextController.clear();
      _highlightedPartController.clear();
    });
  }

  List<Widget> getHighlightedText(String originalString, String inputString) {
    List<Widget> highlightedText;
    // 大文字/小文字の区別をなくすため、すべて小文字に変換
    final String lowerOriginalString = originalString.toLowerCase();
    final String lowerInputString = inputString.toLowerCase();
    // もとの文字列の最後の文字のインデックス
    final int lastOfOriginalString = originalString.length - 1;
    // もとの文字列における入力された文字列の最初の文字のインデックス
    final int firstOfInputString = lowerOriginalString.indexOf(lowerInputString);
    // もとの文字列における入力された文字列の最後の文字のインデックス
    int lastOfInputString;
    final double _fontSize = 24.0;
    final Color _highlightColor = Colors.blue;


    // inputStringと一致する箇所がない場合のエラー(Value not in range: -1)回避
    if (firstOfInputString == -1) {
      return [Container()];
    }

    highlightedText = [
      Text(
        originalString.substring(0, firstOfInputString),
        style: TextStyle(
          fontSize: _fontSize,
        ),
      ),
      Text(
        originalString.substring(firstOfInputString, lastOfInputString + 1),
        style: TextStyle(
          color: _highlightColor,
          fontSize: _fontSize,
        ),
      ),
      Text(
        originalString.substring(lastOfInputString + 1),
        style: TextStyle(
          fontSize: _fontSize,
        ),
      ),
    ];
    return highlightedText;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Highlight Text'),
      ),
      body: Padding(
        padding: EdgeInsets.all(15.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Row(
              children: <Widget>[
                const Text(
                  'Original Text: ',
                  style: TextStyle(fontSize: 16),
                ),
                Flexible(
                  child: originalText == ''
                      ? TextField(
                          controller: _originalTextController,
                          onEditingComplete: () {
                            setState(() {
                              originalText = _originalTextController.text;
                            });
                          },
                        )
                      : Text(
                          originalText,
                          style: const TextStyle(fontSize: 24),
                        ),
                ),
              ],
            ),
            const SizedBox(height: 30.0),
            Row(
              children: <Widget>[
                const Text(
                  'Highlighted Text: ',
                  style: TextStyle(fontSize: 16),
                ),
                inputText == ''
                    ? Text(
                        originalText,
                        style: const TextStyle(fontSize: 24.0),
                      )
                    : Row(
                        children: getHighlightedText(originalText, inputText),
                      ),
              ],
            ),
            const SizedBox(height: 60.0),
            TextField(
              controller: _highlightedPartController,
              enabled: originalText == '' ? false : true,
              decoration: const InputDecoration(hintText: 'Keyword'),
              onChanged: (value) {
                setState(() {
                  inputText = value;
                });
              },
            ),
            const SizedBox(height: 60.0),
            RaisedButton(
              onPressed: refreshScreen,
              child: const Text('Refresh'),
            ),
          ],
        ),
      ),
    );
  }
}

flutter_highlight_demo.gif

日本語でもハイライトされました。

Screenshot_1571062039.png

さいごに

はじめはハイライトのパターンによって条件分岐して必要な数だけTextウィジェットを返すようにしようとしましたが、思いの外考慮する条件が多くてコードが複雑になってしまったためこのようにパターンに関係なく文字列を3分割する形をとりました。

また、よく見るとおわかりいただけると思いますが、ハイライトされた文字列とされていない文字列の間が若干広くなってしまいます。ハイライトされたものとされていないものを見比べなければわからないぐらいの差ではありますが、気になる方はコードを改良して使ってみてください。

フォントサイズやカラーなんかは関数の引数に渡して任意に指定できるようにしてもいいかもしれませんね。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
コメント

自分も似たようなことをやっていたので、コード載せておきます。
1つのWidgetとして切り出して、TextSpanでハイライトしています。

class HighlightedText extends StatelessWidget {
  final String text;
  final String highlightWord;
  final TextStyle defaultStyle;
  final TextStyle highlightStyle;

  HighlightedText({
    @required this.text,
    @required this.highlightWord,
    @required this.defaultStyle,
    @required this.highlightStyle,
  });

  int get _highlightStart => text.indexOf(highlightWord);
  int get _highlightEnd => _highlightStart + highlightWord.length;

  @override
  Widget build(BuildContext context) {
    // indexOf()は、合致する文字列がないと-1を返す。
    if (_highlightStart == -1) {
      return Text(text, style: defaultStyle);
    }
    return RichText(
      text: TextSpan(
        style: defaultStyle,
        children: [
          TextSpan(text: text.substring(0, _highlightStart)),
          TextSpan(
            text: text.substring(_highlightStart, _highlightEnd),
            style: highlightStyle,
          ),
          TextSpan(text: text.substring(_highlightEnd))
        ],
      ),
    );
  }
}

@sankentou コードをシェアしてくださりありがとうございます。とてもすっきりしていてわかりやすいです。TextSpanというものがあったんですね。:sweat:

サービス利用規約に基づき、このコメントは削除されました。
あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした