Flutterアプリケーション開発概論

【AppBar UI】メニューアイコン・ロゴ・プロフィール画像を上部に配置する

この節で学ぶこと

前の節では、背景画像の上に黒いグラデーションを重ねて、文字を読みやすくする方法を学びました。

今回の節では、Home画面の上部にある ヘッダーUI を見ていきます。

画面の一番上に、メニューアイコン、NETAFLIXロゴ、プロフィール画像が並んでいます。さらに、その下には ShowsMoviesGamesNew & Hot のカテゴリタブがあります。

左:メニューアイコン
中央:NETAFLIXロゴ
右:プロフィール画像
下:カテゴリタブ

スマホアプリでは、上部のヘッダーはとても大切です。

ユーザーが画面を開いたとき、最初に目に入る場所だからです。

今回のアプリでは、背景画像の上にヘッダーを重ねているため、ただアイコンを置くだけでは見づらくなることがあります。そこで、上部に黒いグラデーションを入れて、メニューアイコンやロゴが見やすくなるようにしています。


今回見るコード

Home画面の上部ヘッダーは、NetflixHeader クラスで作っています。

class NetflixHeader extends StatelessWidget {
  const NetflixHeader({
    super.key,
    required this.selectedCategory,
    required this.onTapMenu,
    required this.onSelectCategory,
  });

  final HomeCategory selectedCategory;
  final VoidCallback onTapMenu;
  final ValueChanged<HomeCategory> onSelectCategory;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 154,
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            NetflixColors.black,
            Colors.black.withValues(alpha: 0.92),
            Colors.black.withValues(alpha: 0.55),
            Colors.black.withValues(alpha: 0.0),
          ],
          stops: const [0.0, 0.42, 0.72, 1.0],
        ),
      ),
      child: SafeArea(
        bottom: false,
        child: Padding(
          padding: const EdgeInsets.fromLTRB(18, 4, 18, 0),
          child: Column(
            children: [
              Row(
                children: [
                  GestureDetector(
                    behavior: HitTestBehavior.opaque,
                    onTap: onTapMenu,
                    child: const Icon(
                      Icons.menu_rounded,
                      color: NetflixColors.white,
                      size: 32,
                    ),
                  ),
                  const Spacer(),
                  const NetflixWordLogo(height: 28),
                  const Spacer(),
                  ClipRRect(
                    borderRadius: BorderRadius.circular(4),
                    child: Image.network(
                      profileImageUrl,
                      width: 28,
                      height: 28,
                      fit: BoxFit.cover,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 22),
              TopCategoryTabs(
                selectedCategory: selectedCategory,
                onSelectCategory: onSelectCategory,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

少し長く見えますが、中身は大きく分けると次の4つです。

1. ヘッダー全体の高さを決める
2. 黒いグラデーション背景を作る
3. メニュー・ロゴ・プロフィール画像を横に並べる
4. カテゴリタブを下に置く

順番に見ていきましょう。


NetflixHeaderの役割

NetflixHeader は、Home画面の上に表示されるヘッダー部分です。

中には、次の要素があります。

要素役割
メニューアイコンタップするとメニューを開く
ロゴアプリ名や世界観を伝える
プロフィール画像ユーザーがログインしているような雰囲気を出す
カテゴリタブShows / Movies / Games / New & Hot を切り替える
グラデーション背景背景画像の上でもアイコンや文字を見やすくする

このヘッダーは、Home画面の背景画像の上に重なっています。

背景画像は作品によって明るさが変わります。明るい画像の上に白いロゴや白いアイコンをそのまま置くと、見えにくくなることがあります。

そのため、上部だけ黒くして、下に行くほど透明になるグラデーションを入れています。


StatelessWidgetなのはなぜ?

NetflixHeaderStatelessWidget です。

class NetflixHeader extends StatelessWidget {

StatelessWidget は、自分自身では状態を持たないWidgetです。

ただし、NetflixHeader にはカテゴリの選択状態が関係しています。

final HomeCategory selectedCategory;

では、なぜ StatefulWidget ではないのでしょうか。

理由は、NetflixHeader 自体がカテゴリの状態を管理しているわけではないからです。

カテゴリの状態は、親である NetflixHomePage が持っています。NetflixHeader は、その状態を受け取って表示しているだけです。

NetflixHomePageが状態を持つ
↓
NetflixHeaderにselectedCategoryを渡す
↓
NetflixHeaderは受け取った状態を使って表示する

このように、状態を持つ場所と、見た目を作る場所を分けると、コードが整理しやすくなります。


受け取っている値を確認しよう

NetflixHeader は、次の3つの値を受け取っています。

required this.selectedCategory,
required this.onTapMenu,
required this.onSelectCategory,

それぞれの役割は次の通りです。

役割
selectedCategory今どのカテゴリが選ばれているか
onTapMenuメニューアイコンを押したときの処理
onSelectCategoryカテゴリタブを押したときの処理

たとえば、メニューアイコンを押したときは、親から渡された onTapMenu が実行されます。

onTap: onTapMenu,

カテゴリタブを押したときは、onSelectCategory が実行されます。

TopCategoryTabs(
  selectedCategory: selectedCategory,
  onSelectCategory: onSelectCategory,
),

このように、NetflixHeader は見た目を担当し、実際の動きは親から受け取った関数に任せています。


Containerでヘッダー全体を作る

ヘッダー全体は、Container で包まれています。

return Container(
  height: 154,
  decoration: BoxDecoration(
    gradient: LinearGradient(
      ...
    ),
  ),
  child: SafeArea(
    ...
  ),
);

ここでは、ヘッダーの高さと背景を設定しています。

height: 154,

この 154 は、ヘッダー全体の高さです。

今回のヘッダーには、上段と下段があります。

上段:メニューアイコン・ロゴ・プロフィール画像
下段:カテゴリタブ

そのため、少し高さを取っています。

もしこの高さを小さくしすぎると、カテゴリタブが窮屈になります。


ヘッダー背景のグラデーション

ヘッダーの背景には、黒から透明に変わるグラデーションを使っています。

decoration: BoxDecoration(
  gradient: LinearGradient(
    begin: Alignment.topCenter,
    end: Alignment.bottomCenter,
    colors: [
      NetflixColors.black,
      Colors.black.withValues(alpha: 0.92),
      Colors.black.withValues(alpha: 0.55),
      Colors.black.withValues(alpha: 0.0),
    ],
    stops: const [0.0, 0.42, 0.72, 1.0],
  ),
),

これは、上はしっかり黒く、下に行くほど透明にする指定です。

上:黒
中間:少し薄い黒
下:透明

背景画像の上にヘッダーを置くと、画像によってはアイコンやロゴが見えにくくなります。

そこで、上部だけ黒くして、メニューアイコンやロゴが見やすくなるようにしています。

一方で、下まで完全に黒いと、背景画像が隠れすぎてしまいます。

そのため、下に向かって透明にしています。


SafeAreaでスマホ上部の余白に対応する

次に出てくるのが SafeArea です。

child: SafeArea(
  bottom: false,
  child: Padding(
    ...
  ),
),

SafeArea は、スマホの画面上部にあるステータスバーやノッチにUIが重ならないようにしてくれるWidgetです。

iPhoneのように画面上部にノッチがある端末では、何も考えずに上から配置すると、アイコンやロゴが見づらくなることがあります。

SafeArea を使うことで、端末に合わせて自然な余白を作ってくれます。

今回のコードでは、

bottom: false,

としているので、下側の余白は無視しています。

ヘッダーは画面上部のUIなので、上側の安全エリアだけ意識すれば十分です。


Paddingで左右の余白を作る

SafeArea の中には、Padding があります。

padding: const EdgeInsets.fromLTRB(18, 4, 18, 0),

EdgeInsets.fromLTRB は、左・上・右・下の余白を指定する書き方です。

L = Left
T = Top
R = Right
B = Bottom

今回の指定はこうです。

位置余白
18
4
18
0

左右に18ずつ余白を取ることで、メニューアイコンやプロフィール画像が画面の端にくっつきすぎないようにしています。

小さな余白ですが、あるだけで画面がかなり見やすくなります。


Columnで上段と下段を縦に並べる

ヘッダーの中身は、Column で縦に並べています。

child: Column(
  children: [
    Row(
      children: [
        ...
      ],
    ),
    const SizedBox(height: 22),
    TopCategoryTabs(
      selectedCategory: selectedCategory,
      onSelectCategory: onSelectCategory,
    ),
  ],
),

Column は、上から下に並べるためのWidgetです。

今回の並びはこうです。

メニュー・ロゴ・プロフィール画像
↓
余白
↓
カテゴリタブ

上段と下段の間には、SizedBox で余白を入れています。

const SizedBox(height: 22),

この余白があることで、アイコン列とカテゴリタブがくっつきすぎず、見やすくなります。


Rowでメニュー・ロゴ・プロフィール画像を横に並べる

上段は Row で横に並べています。

Row(
  children: [
    GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: onTapMenu,
      child: const Icon(
        Icons.menu_rounded,
        color: NetflixColors.white,
        size: 32,
      ),
    ),
    const Spacer(),
    const NetflixWordLogo(height: 28),
    const Spacer(),
    ClipRRect(
      borderRadius: BorderRadius.circular(4),
      child: Image.network(
        profileImageUrl,
        width: 28,
        height: 28,
        fit: BoxFit.cover,
      ),
    ),
  ],
),

この Row の中では、次のように並んでいます。

メニューアイコン  ロゴ  プロフィール画像

左にメニュー、中央にロゴ、右にプロフィール画像を置くことで、アプリらしいヘッダーになります。


メニューアイコンを表示する

左側のメニューアイコンは、次のコードで表示しています。

GestureDetector(
  behavior: HitTestBehavior.opaque,
  onTap: onTapMenu,
  child: const Icon(
    Icons.menu_rounded,
    color: NetflixColors.white,
    size: 32,
  ),
),

Icons.menu_rounded は、三本線のメニューアイコンです。

Icons.menu_rounded

色は白にしています。

color: NetflixColors.white,

サイズは 32 です。

size: 32,

黒背景や暗い画像の上に置くので、白いアイコンにすると見やすくなります。


GestureDetectorでタップできるようにする

メニューアイコンは、GestureDetector で包まれています。

GestureDetector(
  behavior: HitTestBehavior.opaque,
  onTap: onTapMenu,
  child: const Icon(
    Icons.menu_rounded,
    ...
  ),
),

GestureDetector は、タップやスワイプなどの操作を受け取るためのWidgetです。

ここでは、メニューアイコンをタップしたときに onTapMenu を実行しています。

onTap: onTapMenu,

onTapMenu は、親から渡されている関数です。

この関数の中では、下からメニューを表示する処理が動きます。


HitTestBehavior.opaqueとは?

次の指定も見ておきましょう。

behavior: HitTestBehavior.opaque,

これは、アイコンの見た目より少し広い範囲でもタップを受け取りやすくするための指定です。

スマホアプリでは、見た目のアイコンが小さすぎると、タップしづらくなります。

HitTestBehavior.opaque を使うことで、透明な部分も含めてタップ判定しやすくなります。

小さな指定ですが、操作しやすさに関わる大事な部分です。


Spacerでロゴを中央に寄せる

メニューアイコンとロゴの間、ロゴとプロフィール画像の間には、Spacer が入っています。

const Spacer(),
const NetflixWordLogo(height: 28),
const Spacer(),

Spacer は、余ったスペースを埋めるためのWidgetです。

左右に Spacer を置くことで、ロゴが中央付近に来るようにしています。

メニューアイコン | 余白 | ロゴ | 余白 | プロフィール画像

ただし、左右の要素の幅が完全に同じではない場合、ロゴが数学的にぴったり中央になるとは限りません。

今回のUIでは、見た目として自然に中央に見えるように配置しています。


ロゴを表示する

中央のロゴは、前の節でも使った NetflixWordLogo です。

const NetflixWordLogo(height: 28),

NetflixWordLogo は、assets/images/logo.svg を表示するための部品です。

このように部品化しておくと、いろいろな場所で同じロゴを使いやすくなります。

たとえば、スプラッシュ画面では大きめに表示しています。

const NetflixWordLogo(height: 54),

Home画面のヘッダーでは、少し小さめに表示しています。

const NetflixWordLogo(height: 28),

同じロゴでも、使う場所に合わせて高さだけ変えています。


プロフィール画像を表示する

右側には、プロフィール画像を表示しています。

ClipRRect(
  borderRadius: BorderRadius.circular(4),
  child: Image.network(
    profileImageUrl,
    width: 28,
    height: 28,
    fit: BoxFit.cover,
  ),
),

プロフィール画像は、Image.network で表示しています。

Image.network(
  profileImageUrl,
)

profileImageUrl は、コードの上の方に定義されています。

const profileImageUrl =
    'https://images.unsplash.com/photo-1547425260-76bcadfb4f2c?w=160&h=160&fit=crop';

このURLの画像を、プロフィール画像として表示しています。


ClipRRectで角丸にする

プロフィール画像は、ClipRRect で包まれています。

ClipRRect(
  borderRadius: BorderRadius.circular(4),
  child: Image.network(
    ...
  ),
),

ClipRRect は、中のWidgetを角丸に切り抜くためのWidgetです。

今回の場合は、プロフィール画像の角を少しだけ丸くしています。

borderRadius: BorderRadius.circular(4),

角丸を大きくすると、より丸い見た目になります。

たとえば、完全に丸くしたい場合は、ClipOval を使うこともあります。

ただし、今回のデザインではNetflix風の四角いプロフィールアイコンに近づけるため、少しだけ角丸にしています。


Image.networkのサイズ指定

プロフィール画像には、幅と高さを指定しています。

width: 28,
height: 28,

これで、28×28の小さなプロフィール画像になります。

さらに、

fit: BoxFit.cover,

を指定しているので、画像が枠いっぱいに表示されます。

プロフィール画像のように正方形の枠に入れる場合、BoxFit.cover を使うときれいに収まりやすいです。


カテゴリタブを下に置く

上段の下には、カテゴリタブを表示しています。

TopCategoryTabs(
  selectedCategory: selectedCategory,
  onSelectCategory: onSelectCategory,
),

TopCategoryTabs は、ShowsMoviesGamesNew & Hot のタブを表示する部品です。

ここでは、現在選ばれているカテゴリと、カテゴリを選んだときの処理を渡しています。

selectedCategory: selectedCategory,
onSelectCategory: onSelectCategory,

つまり、NetflixHeader はカテゴリタブそのものの中身までは作っていません。

カテゴリタブの見た目と動きは、TopCategoryTabs に任せています。

部品を分けることで、コードが見やすくなります。


ヘッダーの全体構造を整理しよう

ここまで見た内容を、構造として整理するとこうなります。

NetflixHeader
└── Container
    ├── グラデーション背景
    └── SafeArea
        └── Padding
            └── Column
                ├── Row
                │   ├── メニューアイコン
                │   ├── Spacer
                │   ├── ロゴ
                │   ├── Spacer
                │   └── プロフィール画像
                ├── SizedBox
                └── TopCategoryTabs

このように、複数のWidgetを組み合わせてヘッダーを作っています。

一見長いコードでも、構造を分けて見ると分かりやすくなります。


まずカスタマイズしてみよう

今回は、ヘッダーのロゴサイズを少し大きくしてみましょう。

次のコードを探してください。

const NetflixWordLogo(height: 28),

これを次のように変更します。

const NetflixWordLogo(height: 34),

保存して、Home画面を確認してください。

ヘッダー中央のロゴが少し大きくなります。

ただし、大きくしすぎると、上部のバランスが悪くなることがあります。

見た目を確認しながら、少しずつ調整しましょう。


メニューアイコンのサイズをカスタマイズしてみよう

次に、メニューアイコンのサイズを変えてみます。

変更前はこちらです。

size: 32,

少し小さくする場合は、次のようにします。

size: 28,

保存して、見た目を確認してください。

アイコンが小さくなると、すっきり見える場合があります。

ただし、小さくしすぎるとタップしづらくなるので注意しましょう。


プロフィール画像を少し大きくしてみよう

プロフィール画像のサイズは、次の部分で決まっています。

width: 28,
height: 28,

これを少し大きくしてみます。

width: 34,
height: 34,

保存して、右上のプロフィール画像が大きくなるか確認してください。

プロフィール画像を大きくすると、ユーザー感が少し強くなります。

ただし、ヘッダー内のバランスも変わるので、ロゴやメニューアイコンとの大きさを見ながら調整しましょう。


ヘッダーの高さをカスタマイズしてみよう

ヘッダー全体の高さは、次のコードで決まっています。

height: 154,

少し低くしたい場合は、次のようにできます。

height: 140,

逆に、カテゴリタブとの間隔をゆったり見せたい場合は、次のようにできます。

height: 170,

ヘッダーの高さを変えると、Home画面上部の印象も変わります。

狭くするとコンパクトになり、広くするとゆったりした印象になります。


よくあるつまずきポイント

Q. メニューアイコンを押しても反応しません。

次の部分を確認してください。

onTap: onTapMenu,

また、親側で onTapMenu にメニューを開く処理が渡されているかも確認しましょう。

NetflixHeader(
  selectedCategory: selectedCategory,
  onTapMenu: onTapMenu,
  onSelectCategory: onSelectCategory,
),

onTapMenu が正しく渡されていないと、タップしても何も起きません。

Q. ロゴが表示されません。

NetflixWordLogo が使っている画像パスを確認してください。

SvgPicture.asset(
  'assets/images/logo.svg',
)

さらに、pubspec.yaml にアセット登録があるか確認します。

flutter:
  uses-material-design: true
  assets:
    - assets/images/

Q. プロフィール画像が表示されません。

profileImageUrl のURLが正しいか確認してください。

const profileImageUrl =
    'https://images.unsplash.com/photo-1547425260-76bcadfb4f2c?w=160&h=160&fit=crop';

ネットワーク画像なので、通信環境によって表示に時間がかかることもあります。

Q. ヘッダーが背景画像と重なって見づらいです。

グラデーションの濃さを調整してみましょう。

Colors.black.withValues(alpha: 0.55),

を少し濃くする場合は、次のようにできます。

Colors.black.withValues(alpha: 0.70),

ヘッダーの文字やアイコンが見やすくなります。


チャレンジ

チャレンジ1:ヘッダー中央のロゴを大きくしよう

次のコードを探してください。

const NetflixWordLogo(height: 28),

これを次のように変更します。

const NetflixWordLogo(height: 34),

ロゴの見え方がどう変わるか確認しましょう。

チャレンジ2:メニューアイコンを少し小さくしよう

次のコードを探してください。

size: 32,

これを次のように変更します。

size: 28,

左上のメニューアイコンが少し小さくなります。

チャレンジ3:プロフィール画像を少し大きくしよう

次のコードを探してください。

width: 28,
height: 28,

これを次のように変更します。

width: 34,
height: 34,

右上のプロフィール画像が大きくなるか確認してください。

チャレンジ4:ヘッダーの高さを変えてみよう

次のコードを探してください。

height: 154,

これを次のように変更します。

height: 170,

ヘッダー全体が少しゆったり表示されます。


チャレンジの答え

チャレンジ1の答え

変更前:

const NetflixWordLogo(height: 28),

変更後:

const NetflixWordLogo(height: 34),

ヘッダー中央のロゴが少し大きくなります。

チャレンジ2の答え

変更前:

size: 32,

変更後:

size: 28,

左上のメニューアイコンが少し小さくなります。

チャレンジ3の答え

変更前:

width: 28,
height: 28,

変更後:

width: 34,
height: 34,

右上のプロフィール画像が少し大きくなります。

チャレンジ4の答え

変更前:

height: 154,

変更後:

height: 170,

ヘッダー全体の縦幅が広くなります。


この節のまとめ

この節では、Home画面上部にあるメニューアイコン、ロゴ、プロフィール画像を配置するヘッダーUIを学びました。

大切なポイントは次の通りです。

  • NetflixHeader は、Home画面上部のヘッダーを作るクラス。
  • ヘッダーには、メニューアイコン、ロゴ、プロフィール画像、カテゴリタブがある。
  • Container でヘッダー全体の高さと背景を作っている。
  • 背景には、上が黒く下が透明になるグラデーションを使っている。
  • SafeArea を使うと、スマホ上部のノッチやステータスバーに対応できる。
  • Padding で左右に余白を作ると、UIが端に寄りすぎない。
  • Row を使うと、メニューアイコン、ロゴ、プロフィール画像を横に並べられる。
  • GestureDetector を使うと、アイコンをタップできるようになる。
  • Spacer を使うと、余白を使って要素の位置を調整できる。
  • ClipRRect を使うと、プロフィール画像を角丸にできる。
  • TopCategoryTabs を別部品にすることで、ヘッダーのコードが見やすくなる。

次のステップ

次の節では、ShowsMoviesGamesNew & Hot のカテゴリタブを詳しく見ていきます。

タブを押したときに、Home画面の作品一覧がどのように切り替わるのかを確認します。

ここから、ただ見た目を作るだけでなく、ユーザー操作に合わせて表示内容を変える仕組みを学んでいきましょう。

教材トップへ戻る