[読書会]セーフエリアとMediaQuery

現代のスマートフォンやタブレットは、ディスプレイデザインが多様化しており、画面の一部にノッチやカメラの切り欠き、丸みを帯びた角などが存在します。これにより、通常のUIレイアウトが意図しない形で見えなくなったり、操作しにくくなったりすることがあります。

SafeAreaは、こうしたデバイスの物理的な特性による問題を解決するために、Flutterが提供するウィジェットです。具体的には、子ウィジェットを画面の安全な領域に配置するために使用されます。安全な領域とは、ノッチやステータスバー、画面の丸みを避けた領域のことです。

1.SafeArea

1-1.SafeArea でラップする

(例)SafeArea でラップしたウィジェットは、上記の障害物に干渉されなくなります:
@override
Widget build(BuildContext context) {
  return Scaffold(
    //SafeAreaでラップしてセーフエリア内に背景画像とコンテンツを配置
    body: SafeArea(
      child: Container(
        color: Colors.red.withOpacity(0.5), //パディング領域を赤で表示
        child: Stack(
          children: [
            //セーフエリア内に背景画像を表示
            Positioned.fill(
              child: Image.network(
                'https://via.placeholder.com/1080x1920.png?text=SafeArea+Background',
                fit: BoxFit.cover,
              ),
            ),
            //コンテンツ
            const Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'このテキストはSafeArea内にあります',
                  style: TextStyle(fontSize: 24, color: Colors.white),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ],
        ),
      )
    ),
  );
}

1-2.SafeArea の4つの側面の設定

SafeAreaウィジェットは、デフォルトで子ウィジェットを4つの側面すべて(上、下、左、右)から安全な領域内に配置しますが、特定の側面についてはこのパディングを無効にすることができます。これにより、より柔軟にUIをレイアウトできるようになります。

(例)SafeArea の4つの側面の設定(有効、無効)
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SafeArea Padding Example',
      home: const SafeAreaPaddingExample(),
    );
  }
}

class SafeAreaPaddingExample extends StatelessWidget {
  const SafeAreaPaddingExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      //SafeAreaを使用して、特定の側面のパディングを無効化
      body: SafeArea(
        left: false,  //左側のパディングを無効にする
        top: true,    //上側のパディングを有効にする(デフォルト)
        right: false, //右側のパディングを無効にする
        bottom: true, //下側のパディングを有効にする(デフォルト)
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const [
            Text(
              'このテキストは上と下にのみパディングがあります。',
              style: TextStyle(fontSize: 24),
              textAlign: TextAlign.center,
            ),
            SizedBox(height: 20),
            Text(
              '左と右のパディングは無効です。',
              style: TextStyle(fontSize: 18),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}

1-3.SafeArea のラップする位置は Scaffold 本体 に限らない

多くのアプリでは、Scaffoldのbody内のすべてのコンテンツが画面の物理的な制約を避ける必要があるため、SafeAreaをbody全体に適用するのが一般的です。これにより、コンテンツ全体が安全な領域に配置されます。
特定のコンテンツだけにSafeAreaを使う例:

場合によっては、ウィジェットツリー全体ではなく、特定の部分だけをSafeAreaで保護したいことがあります。このような場合、SafeAreaをウィジェットツリーの深い位置に配置して、必要な部分だけを保護することができます。

以下は、特定の部分だけを、SafeArea でラップした例です。

(例)SafeArea を(Scaffold.body 全体ではなく)一部だけラップした:
import 'package:flutter/material.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Partial SafeArea Example',
      home: PartialSafeAreaExample(),
    );
  }
}

class PartialSafeAreaExample extends StatelessWidget {
  const PartialSafeAreaExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          //背景全体を表示するためにSafeAreaは適用しない
          Positioned.fill(
            child: Image.network(
              'https://via.placeholder.com/1080x1920.png?text=Background+Image',
              fit: BoxFit.cover,
            ),
          ),
          //特定のコンテンツ(AppBarや重要なテキスト)のみをSafeAreaで保護
          Column(
            children: [
              SafeArea(
                child: Container(
                  color: Colors.black.withOpacity(0.5),
                  child: const Text(
                    'このテキストはSafeAreaで保護されています。',
                    style: TextStyle(fontSize: 24, color: Colors.white),
                    textAlign: TextAlign.center,
                  ),
                ),
              ),
              const Spacer(),
              Container(
                color: Colors.red.withOpacity(0.5),
                child: const Text(
                  'このテキストはSafeAreaで保護されていません。',
                  style: TextStyle(fontSize: 24, color: Colors.white),
                  textAlign: TextAlign.center,
                ),
              ),
              const SizedBox(height: 20),
            ],
          ),
        ],
      ),
    );
  }
}
//このように、重要なコンテンツ(ナビゲーションバーや特定のテキスト)だけをSafeAreaで保護し、他の部分はフルスクリーンで表示したい場合に役立ちます。例えば、背景画像をフルスクリーンで表示しつつ、ナビゲーションや重要なUI要素だけを保護するデザインが求められる場合です。

1-4.SafeArea は内部で MediaQuery を使用している

SafeAreaは、アプリのコンテンツをデバイスの物理的な制約やシステムUIから保護するための強力なツールです。その内部ではMediaQueryを使用してセーフエリアを自動的に計算し、少ないコードで多くのことを実現しています。SafeAreaを使うことで、新しいデバイスや異なる画面形状にも柔軟に対応できるアプリケーションを作成することができます。

1-5.役立つブログの紹介

なお、上記補足では、SafeArea の良い例を用意できなかったのですが、
以下の、sakushin さんのブログが、とってもよく理解できましたので、ご紹介致します。

このブログ中の、図解や説明が、とても分かり易かったです。<(_ _)!!>

2.MediaQuery

(1)MediaQueryの機能:

MediaQuery は(MediaQueryData情報(以下の「2-2.補足」参照)を提供する、等)適応型デザインに欠かせません。
即ち、次の機能を(直接的(明示的)に、または間接的に(裏で、上記1の)SafeArea を経由して)提供します。

・MediaQuery により画面サイズだけでなく、多くの情報(以下の「2-2.補足」参照)を取得することができます。
・SafeArea は、このMediaQuery により、paddingプロパティを使用して、セーフエリアを制御しています。

(2)SafeArea が優れている理由:

しかし、だからといって、セーフエリアを制御したい場合には、MediaQueryData を直接使用するのではなく、やはり SafeArea を使用することにより、セーフエリア制御を実現する方が、以下の理由により、適切であると言えます。

・MediaQueryData を直接使用すると、その度にpadding を何度も使用する必要があります。
 しかし、SafeArea を利用することで、padding の明示的な制御を減らすことができます。

(3)SafeArea は部分的に使用できます:

SafeAreaをScaffoldのbody全体に適用することは可能ですが、必ずしも必要ではありません。
SafeAreaは、ノッチやシステムUIによって情報が切り取られる可能性がある部分にだけ適用すれば十分です。
アプリの要件に応じて、どの部分にSafeAreaを適用するかを決めることで、効率的なレイアウト設計が可能になります。

(例)SafeAreaをScaffoldのbody全体に適用する:
Scaffold(
  body: SafeArea(
    child: Text('画面全体がSafeAreaで保護されます'),
  ),
);
(例)SafeAreaをツリーの下位に適用する:
Scaffold(
  body: Column(
    children: [
      SafeArea(
        child: Text('この部分だけSafeAreaで保護されます'),
      ),
      Text('この部分は保護されません'),
    ],
  ),
);

(4)SafeArea は、Scaffold をラップしてはいけません:

①AppBar は、SafeArea で囲まなくてもデフォルトで、セーフエリアに設定されているので、AppBar ウィジェットは、なにもしなくても、ステータスバーの下に自然に配置される様になっています。
②故に、(上記(3)の例では、body 以下を SafeArea でラップしていますが)SafeArea で Scaffold をラップしてはいけません。
 上記①も、その理由のうちの1つです。
③それに、もし、SafeArea で Scaffold をラップしてしまうと、(上記①でデフォルトで正確に動作するはずの)AppBar の表示位置が、予想外に下にずれたりするからです。

(悪い例)SafeArea で、Scaffold 全体をラップしてはいけません:
SafeArea(
  child: Scaffold(
    appBar: AppBar(
      title: Text('App Title'),
    ),
    body: Text('コンテンツ'),
  ),
);

(補足)MediaQueryData のコンストラクタ

上記(2.MediaQuery)に関して、MediaQueryData のコンストラクタについての補足です。

①MediaQueryData.fromView / 名前付きコンストラクタ

MediaQueryData には2つのコンストラクタ(MediaQueryData()、MediaQueryData.fromView())があります。
この2つ目の MediaQueryData.fromView() は、名前付きコンストラクタと呼ばれるものです。

MediaQueryData の 名前付きコンストラクタ:
MediaQueryData.fromView(
 FlutterView view, 
 {MediaQueryData? platformData}
)
//第1引数の FlutterView は、
//Flutterアプリケーションの描画やユーザーインターフェースの
//レンダリングを担当するウィンドウのようなもので、
//画面や表示に関する情報を持っています。

このコンストラクタを使用すると、明示的に値を設定する代わりに、
FlutterViewから得られる情報を基に、動的MediaQueryDataを生成することができます。

(例)上記の(名前付き)コンストラクタで、MediaQueryDataを生成します:
final mediaQueryData = MediaQueryData.fromView(
 WidgetsBinding.instance.platformDispatcher.views.first
);
//このコードでは、
//WidgetsBinding.instance.platformDispatcher.views.first
//を使用して、アプリケーションの最初のビューから
//MediaQueryDataを生成しています。

②MediaQueryData() / 標準コンストラクタ

これは標準のコンストラクタになります。
上記①が動的にMediaQueryData を取得するのに対し、こちらは、画面サイズやピクセル密度等を固定値で設定するので、開発者が意図的に固定値を設定したい場合に使用するようです。

MediaQueryData の標準コンストラクタ:
MediaQueryData({
  //デバイス画面のサイズ:
  Size size = Size.zero,
  //デバイスのピクセル密度:
  double devicePixelRatio = 1.0,
  //テキストのスケーリング:
  TextScaler textScaler = TextScaler.custom(scale: 1.5),
  //デバイスのプラットフォームの明るさ:
  Brightness platformBrightness = Brightness.light,
  //安全領域の設定:
  EdgeInsets padding = EdgeInsets.zero,
  //隠された部分のインセット情報:
  //キーボードやソフトウェアによって画面の一部が
  //覆われている領域を表します。たとえば、
  //キーボードが表示されたときの隠された部分のインセット情報です。
  //(例)キーボードが表示されるとviewInsets.bottomに
  //その高さが設定されます。
  EdgeInsets viewInsets = EdgeInsets.zero,
  //システムジェスチャー(スワイプやナビゲーションバーなど)
  //が重なる可能性のある領域を指定します。
  //(例)iPhoneのホームインジケーターがある部分のインセットです。
  EdgeInsets systemGestureInsets = EdgeInsets.zero,
  //ディスプレイ上の物理的に使用できない領域
  //(セーフエリアやノッチ部分)のパディング情報です。
  //通常のパディングと異なり、ソフトウェアUI(キーボードなど)
  //によって変化しません。
  //(例)viewPadding.topはステータスバーの高さを意味します。
  EdgeInsets viewPadding = EdgeInsets.zero,
  //デバイスが24時間形式を使用するかどうかを示します。
  //trueに設定すると、時計が24時間形式で表示されます。
  //(例)alwaysUse24HourFormat: trueで24時間表示を強制します。
  bool alwaysUse24HourFormat = false,
  //アクセシビリティ機能が有効かどうかを示します。
  //trueなら、視覚的なアニメーションなどを減らすべき場合を表します。
  bool accessibleNavigation = false,
  //色が反転して表示されているかどうかを示します。
  //アクセシビリティ向けの設定で、trueにすると色が反転します。
  bool invertColors = false,
  //高コントラストモードが有効かどうかを示します。
  //視覚障害者向けにコントラストを強調する設定です。
  bool highContrast = false,
  //スイッチのオン/オフラベルが表示されるかどうかを示します。
  //オン/オフの表示が視覚的に追加されます。
  //onOffSwitchLabels: true
  //でスイッチに「ON」や「OFF」のラベルが付きます。
  bool onOffSwitchLabels = false,
  //アニメーションを無効にするかどうかを指定します。
  //trueにすると、アニメーションが無効になります。
  bool disableAnimations = false,
  //太字テキストの表示が有効かどうかを示します。
  //アクセシビリティ向けにテキストが太字になる設定です。
  //(例)boldText: trueで、全てのテキストが太字になります。
  bool boldText = false,
  //ナビゲーションモードを表します。
  //NavigationMode.traditionalは従来のボタンナビゲーション
  //NavigationMode.directionalは方向キーやジェスチャーを
  //意味します。
  //(例)navigationMode: NavigationMode.directionalで、
  //ジェスチャーベースのナビゲーションを設定します。
  NavigationMode navigationMode 
   = NavigationMode.traditional,
  //ジェスチャー操作感度(スワイプ,タッチ操作の閾値等)
  //を設定します。
  //(例)gestureSettings:DeviceGestureSettings(touchSlop: 8.0)
  //で、ジェスチャー感度を調整。
  DeviceGestureSettings gestureSettings 
   = const DeviceGestureSettings(
         touchSlop: kTouchSlop),
  //ディスプレイの特徴(折り畳み部分やヒンジなど)を表します。
  //折り畳み式のデバイスで表示領域が分割される部分を含みます。
  //(例)折り畳み式デバイスのヒンジ位置などを示すリストです。
  List<DisplayFeature> displayFeatures = 
   const <ui.DisplayFeature>[],
  //システムのコンテキストメニューを表示できるかどうかを示します。
  //trueにすると、システムのコンテキストメニューが有効になります。
  //(例)supportsShowingSystemContextMenu: trueで、
  //コンテキストメニューをサポートします。
  bool supportsShowingSystemContextMenu = false
})
//このコードでは意図的に固定値を設定しています。

3.まとめ

(1)SafeArea は、アプリのコンテンツが切り取られないようにします。

(2)SafeArea は、新しいデバイスにも対応できる設計を提供します。

(3)SafeArea は、汎用的にアプリのコンテンツを保護します。

コメントを残す