8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-11-05

はじめに

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

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分割する形をとりました。

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

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

8
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?