Flutter でボタンを押すと伸縮する Text を作る

5 min read読了の目安(約4500字

はじめに

長いテキスト等を表示する際に、最初は一部だけ表示しておいて、ボタン等を押すと全文を表示するような UI があると思います。

Folio Udemy ミュージック
folio_screenshot udemy_screenshot music_screenshot

iOS 標準のミュージックアプリだけ少し挙動が違っていて、「さらに表示」を押すとモーダルで全文が表示されます。

Flutter で上記のような UI を作ろうとして、意外と情報がなかったのでまとめてみます。
また、今回作った物は全て以下のリポジトリにアップしてあります 🙇‍♂️

https://github.com/hayabusabusa/zenn_expandable_text_sample

作るもの

以下の要件を満たすものを作成します。

  • 初期状態では数行のみテキストを表示する
  • ボタン等を押すと全文を表示するように切り替える
  • テキストが短い場合はボタン等を表示しない

実装

それでは実際に作ってみます。

表示したいテキストの行数やサイズは描画後にしかわからないので、方針としては LayoutBuilderTextPainer を使用して描画されるテキストの行数、サイズを取得し、最大行数に達しているかどうかで表示する Widget を切り替えるようにします。

LayoutBuilder

LayoutBuilder には Widget のサイズを取得してもらい、テキストの描画の際に取得したサイズから幅の情報を取り出して使用します。


  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (_, constraints) {
      // `constrains` に入っているサイズの情報を利用してテキストの描画等を行う
    });
  }

https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html

TextPainter

TextPainter には Text に適応させる TextStyle を指定した上でテキストの描画を行ってもらい、描画したテキストが最大行数に達しているかどうかを判断してもらいます。


  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (_, constraints) {
      // テキストのスタイル、最大行数等を指定して `TextPainter` を作成
      final textStyle = widget.style ?? DefaultTextStyle.of(context).style;
      final textSpan = TextSpan(text: widget.data, style: textStyle);
      final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr, maxLines: widget.maxLines);

      // テキストの描画を行う
      textPainter.layout(maxWidth: constraints.maxWidth);

      if (textPainter.didExceedMaxLines) {
        // 最大行数よりも多いテキストの場合
      } else {
        // 最大行数よりも少ないテキストの場合
      }
    });
  }

https://api.flutter.dev/flutter/painting/TextPainter-class.html

できたもの

上記2つをまとめて1つの StatefulWidget にしてみました。
見た目、動作としては以下のようになります。

短いテキストにはボタンが表示されず、長いテキストのみボタンが表示されていて、かつボタンを押すと開いたり閉じたりできるようになりました。

Dec-04-2020 18-41-26

ソースコード
expandable_text.dart
import 'package:flutter/material.dart';

class ExpandableText extends StatefulWidget {
  const ExpandableText(
    this.data, {
      Key key,
      this.maxLines = 3,
      this.textOverflow = TextOverflow.fade,
      this.style,
    }
  ): super(key: key);

  final String data;
  final int maxLines;
  final TextOverflow textOverflow;
  final TextStyle style;

  
  _ExpandableTextState createState() => _ExpandableTextState();
}

class _ExpandableTextState extends State<ExpandableText> with SingleTickerProviderStateMixin {
  bool _isExpanded = false;

  
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (_, constraints) {
      final textStyle = widget.style ?? DefaultTextStyle.of(context).style;
      final textSpan = TextSpan(text: widget.data, style: textStyle);
      final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr, maxLines: widget.maxLines);

      textPainter.layout(maxWidth: constraints.maxWidth);

      if (textPainter.didExceedMaxLines) {
        return Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              widget.data,
              style: textStyle,
              overflow: widget.textOverflow,
              maxLines: _isExpanded ? null : widget.maxLines,
            ),
            const SizedBox(height: 4.0,),
            FlatButton(
              onPressed: () {
                setState(() {
                  _isExpanded = !_isExpanded;
                });
              }, 
              child: Text(
                _isExpanded ? '閉じる' : '開く'
              ),
            )
          ],
        );
      } else {
        return Text(
          widget.data,
          style: textStyle,
          maxLines: widget.maxLines,
        );
      }
    });
  }
}

さいごに

Column の部分を Stack にしたりすれば iOS 標準のミュージックアプリっぽいものも作れそうです。

あとは伸縮の際にアニメーションさせることができればもっといい感じになりそうですね 🤔

参考