FlutterでUIKitのNSMutableAttributedStringのようにレンジを指定してTextを装飾する
見出し画像

FlutterでUIKitのNSMutableAttributedStringのようにレンジを指定してTextを装飾する

ugo

こんにちは、株式会社 Zaim で iOS エンジニアをしている ugo です。

皆さん Flutter を触っていますか?
最近、アプリ開発の界隈では Flutter の勢いがすごいですよね!
Zaimでも Yoica というアプリを Flutter を採用して開発しています。

私も3月から個人で開発している iOSアプリを Flutter でリプレイスしたいなと思い、勉強を始めました。
個人アプリをリプレイスするにあたって Flutter のTextウィジェットを、 iOSの UIKit の NSMutableAttributedString のように文字列の範囲を指定し装飾する必要がありました。
そこでカスタムしたTextウィジェットを自作してみましたので共有します。

UIKitのテキスト装飾

Flutter の前に軽く UIKit の NSMutableAttributedString を使った実装例を紹介します。
サンプルで下記のような文字列があったとして、”Yoica” と “Flutter” の部分をハイライトで装飾したいとします。

"ZaimではYoicaをFlutterで開発しています”

そうすると UIKit では下記のように装飾したい文字の始点と長さを指定すると Label が装飾できますよね。

 func setupLabel() {
       let attributedString = NSMutableAttributedString(string: "ZaimではYoicaをFlutterで開発しています")
       attributedString.addAttributes([.foregroundColor: UIColor.orange], range: NSMakeRange(6, 5))
       attributedString.addAttributes([.foregroundColor: UIColor.blue], range: NSMakeRange(12, 7))
       label.attributedText = attributedString
   }

名称未設定

Flutterのテキスト装飾

では Flutter ではどうでしょう?
Flutter では Text の装飾は RichText を使います。
しかし NSAttributedString のように装飾したい範囲を指定して実装するような方法はなさそうで、
直接下記のように装飾したい文字列をTextSpanに入れて実装する必要があります。

RichText(
  text: const TextSpan(
      style: TextStyle(
        color: Colors.black,
      ),
      children: [
        TextSpan(text: 'Zaimでは'),
        TextSpan(
            text: 'Yoica',
            style: TextStyle(color: Colors.orange, fontSize: 17)),
        TextSpan(text: 'を'),
        TextSpan(
            text: 'Flutter',
            style: TextStyle(color: Colors.blue, fontSize: 17)),
        TextSpan(text: 'で開発しています'),
      ]))

今回、私の個人アプリではユーザー側で指定した文字列の範囲を装飾する必要があり標準的な RichText の使用方法では実装できませんでした。
そこで RangeHighlightText という名前でRichTextをカスタムしたウィジェットを実装してみました。

実装したもの

class RangeHighlightText extends StatelessWidget {
 const RangeHighlightText(
     {
       required this.baseText,
       required this.highlightRanges,
       this.maxLines,
       this.textAlign = TextAlign.left,
       this.baseTextStyle = const TextStyle(
         color: Colors.black,
       ),
     });
 final String baseText;
 final List<HighlightRange> highlightRanges;
 final int? maxLines;
 final TextAlign textAlign;
 final TextStyle baseTextStyle;

 @override
 Widget build(BuildContext context) {
   int start = 0;
   List<InlineSpan> children = [];
   List<HighlightRange> dedupeHighlightRanges = [];

   highlightRanges.sort((a,b) => a.location.compareTo(b.location));

   for (var highlightRange in highlightRanges){
     if (dedupeHighlightRanges.where((range) => range.location + range.length > highlightRange.location).isEmpty) {
       dedupeHighlightRanges.add(highlightRange);
     }
   }
   nonHighlightAdd(int start, int end) => children
       .add(TextSpan(text: baseText.substring(start, end), style: baseTextStyle));

   highlightAdd(TextStyle textStyle, int start, int end) => children
       .add(TextSpan(text: baseText.substring(start, end), style: textStyle));

   for (var highlightRange in dedupeHighlightRanges){
     if(start == highlightRange.location) {
       highlightAdd(highlightRange.highlightStyle, start, start + highlightRange.length);
     } else {
       nonHighlightAdd(start, highlightRange.location);
       highlightAdd(highlightRange.highlightStyle, highlightRange.location, highlightRange.location + highlightRange.length);
     }
     start = highlightRange.location + highlightRange.length;
   }
   if (start != baseText.length) {
     nonHighlightAdd(start, baseText.length);
   }
   return RichText(
       maxLines: maxLines,
       text: TextSpan(children: children, style: baseTextStyle),
       textAlign: textAlign,
       textScaleFactor: MediaQuery.of(context).textScaleFactor);
 }
}

class HighlightRange {
 final TextStyle highlightStyle;
 final int location;
 final int length;
 HighlightRange({required this.highlightStyle, required this.location, required this.length});
}

呼び出し側

class _MyHomePageState extends State<MyHomePage> {
 List<HighlightRange> highlightRanges = [
   HighlightRange(
       highlightStyle: const TextStyle(color: Colors.orange, fontSize: 17),
       location: 6,
       length: 5),
   HighlightRange(
       highlightStyle: const TextStyle(color: Colors.blue, fontSize: 17),
       location: 12,
       length: 7),
 ];

 String baseText = 'ZaimではYoicaをFlutterで開発しています';

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: Text(widget.title),
     ),
     body: Center(
       child: RangeHighlightText(baseText: baseText, highlightRanges: highlightRanges)
     ), // This trailing comma makes auto-formatting nicer for build methods.
   );
 }
}

呼び出し側で装飾したいレンジの配列とベースとなるテキストを渡して、レンジの配列分を繰り返し処理の中で装飾する文字列としない文字列に切り取って RichTextのTextSpan に詰めています。

これで UIKit の NSAttributedString のようにレンジを指定してTextの装飾ができました!

画像2

最後に 

なかなか使う機会が限られてはいますが何かの実装の参考になりますと嬉しいです。また、こんな風に実装してみては?などアドバイスありましたらコメントで教えてもらえますと幸いです。

Zaim では、エンジニアを募集しています。ぜひ話を聞きに来てください!





この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
ugo