そのアプリ…文字サイズをデカくしてもUI崩れないですか？

## はじめに

[Flutter #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/flutter-2)の13日目の記事です。

[株式会社Tenxia](https://tenxia.jp/)のCEOの岡部（[Twitter](https://twitter.com/_masaokb)）です。 

この記事の内容は[Flutter Meetup Tokyo #13](https://flutter-jp.connpass.com/event/155559/)の自身のLTを記事に起こしたものです。

[![スライド](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530822/6db15fdb-61c6-2b4f-8020-aa5fe4aaaf8e.png)](https://docs.google.com/presentation/d/1hvSX3ZzXPZZr3HeoBNhJiqSff-bPPa-UhhszgmClN2Q/edit?usp=sharing)

## これはなに

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530822/5fb8143d-bc00-7075-2053-c100c48102ad.png" width="200">

- 高齢化社会日本
    - 2020年 女性の過半数が50歳以上になる
    - 2025年 国民の1/3が高齢者になる
    - 2065年 国民の2.5人に1人が高齢者に
- 今やスマホアプリは若者だけでなく**全ての年齢層**が使うもの

つまり

- 日本市場向けアプリを作るからには**大きい文字サイズは意識**したほうが良い
    - エンジニア・デザイナーは若いので、**自分の基準で小さすぎる文字サイズを設定しがち**…
- 一方で、大きい文字サイズを嫌がるユーザも存在する

であるからして

- 文字サイズを固定にするのでなく、**OSなりアプリなりで制御できる**ようにするべき

ということは

- **幅広い文字サイズで表示可能で、なおかつ崩れないUIが必須！**

ところで

- **Flutterはめちゃくちゃ簡単に（10分ぐらいで）文字サイズ調整対応ができる**のでトライしてみよう


## 文字サイズの変更を許容した場合に発生する要件

- 変なところで改行しないか？
- 機能的に文字サイズが変化すべきでないWidgetではないか？
- 横に配置したプロフィール画像が変な位置に表示されないか？
- 省略すべきでないテキストが省略されないか？
- 大きいデバイスだと大丈夫だけど小さいデバイスで潰れたりしないか？

下の画像はTwitterのiOSアプリですが、

- プロフィールアイコンは文字サイズに合わせて拡大している
- RT数やlike数は拡大していない
- 時間は省略されない

など、前述の要件が考慮されているのが分かります。

|文字サイズ最小|文字サイズ最大|
|---|---|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530822/406d6a65-b3ea-71e1-2bc3-453630b4d36d.png" width="300">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530822/c360e2a4-05f3-e6b3-1f8d-abebb661e25a.png" width="300">|


## 今回作ったもの
[Tenxia-inc/font_size_changeable](https://github.com/Tenxia-inc/font_size_changeable)

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530822/f293d506-2ac4-6a99-cf23-f64daa50c4d9.gif)


## しくみ

- FlutterにはFontSizeとは別に、[TextScaleFactor](https://api.flutter.dev/flutter/widgets/Text/textScaleFactor.html)というpropertyがあります
  - 全てのTextに作用する乗数（たとえば2.0なら、全てのテキストが2倍のサイズになる）
  - OSのフォントサイズも、デフォルトではここに作用している
    - `MediaQuery.of(context).textScaleFactor`で取得できる
  - また、各WidgetにもTextScaleFactorを設定でき、そちらが優先されるので、マクロのTextScaleFactorを使いたくないときは明示的に記載すれば良い
- MaterialAppにはbuilderという引数があり、そこでMediaQueryでラップしたWidgetを返すことで、アプリ全体のTextScaleFactorを設定することができる
  - ここではplatformの設定とアプリ内の文字サイズを乗算し、一定の範囲内に絞った上で利用


```dart
      builder: (context, snapshot) => MaterialApp(
        title: 'Font Size Changeable',
        home: const FontSizeSettingPage(),
        builder: (BuildContext context, Widget child) {
          final platformFactor = MediaQuery.of(context).textScaleFactor ?? 1;
          final multipliedFactor = platformFactor * snapshot.data;
          final adjustedFactor =
              min(max(_minFactor, multipliedFactor), _maxFactor);
          logger.info('Set textScaleFactor: $adjustedFactor');
          return MediaQuery(
            data: MediaQuery.of(context)
                .copyWith(textScaleFactor: adjustedFactor),
            child: child,
          );
        },
```

という前提の下、3種類のText Widgetを使用しています。

### 通常のText

- 普通にText Widgetを表示してあげると、そのまま文字サイズが変更されます。
- 改行位置を気にしない、メインのコンテンツやユーザコメントなどに。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530822/b535d05e-cfcd-7aa7-ff52-2c0709ac82ea.png" width="300">

```dart
          const Text(
            '普通に文字サイズが変更されてほしいテキスト。\n'
            'メインのコンテンツとかですね',
          ),
```

### 文字サイズ不変のText
- 一方、TextのtextScaleFactorに固定値を設定してあげると、文字サイズが変化しないWidgetになります。
- 各種ボタンや、一部のラベルなど、文字サイズが変更してほしくない場合に。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530822/74f95eea-c5e1-291a-6573-d17091983e79.png" width="300">

```dart
                const Text(
                  '不変であってほしいテキスト。',
                  textScaleFactor: 1,
                  textAlign: TextAlign.right,
                ),
```

### AutoSizeText
- [AutoSizeText](https://pub.dev/packages/auto_size_text)という、与えられたサイズに応じて文字サイズを自動で変更してくれるライブラリがあり、一定以上は大きくならないWidgetを作れます。
- 説明文や注釈など、変なところで改行してほしくない場合に。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530822/20d82bcc-51f5-20e2-e0ea-7cba7b630f51.png" width="300">

👇自分は改行文字の個数に応じて自動でmaxLinesを判定してくれるWidgetを自作して使っています。

```dart
@immutable
class FitText extends StatelessWidget {
  final String text;
  final int maxLines;
  final EdgeInsets padding;
  final TextStyle style;
  final TextAlign textAlign;

  const FitText(
    this.text, {
    this.maxLines,
    this.padding = EdgeInsets.zero,
    this.style,
    this.textAlign,
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final lines = maxLines ?? text.split('\n').length;
    return Padding(
      padding: padding,
      child: AutoSizeText(
        text,
        textAlign: textAlign,
        maxLines: lines,
        style: style,
        minFontSize: 1,
      ),
    );
  }
}
```

### TextWidget以外
プロフィールアイコンなど、比例して大きくなるべきWidgetも、
Sizeに`MediaQuery.of(context).textScaleFactor`を乗算してやればいい感じになります。

## まとめ

- という風に、Flutterでは簡単に文字サイズ変更に対応できます。
  - 各ネイティブでこれを実現しようと思うとめちゃくちゃ大変なはず。
- Flutter歴2ヶ月の新参者ですが、Flutter、かなりメインストリームになってきていると思います。
- 会社でもう一記事、[このAdvent Calenderに寄稿](https://qiita.com/Kurusu_ima/items/881c5ee96a42b8966e62)してるので、よろしければどうぞ。
- 良いFlutter Lifeを！