
【Home画面UI】大きな背景画像・ロゴ・Playボタンを重ねて表示する
この節で学ぶこと
前の節では、下部ナビゲーションを使って、Home・Clips・Search・My Netaflixの画面を切り替える仕組みを学びました。
今回の節では、いよいよHome画面の上部を作っている部分を見ていきます。
このアプリのHome画面では、最初に大きな背景画像が表示され、その上にロゴ、作品タイトル、カテゴリ情報、Playボタン、Infoボタンなどが重なっています。
見た目としては、かなり動画アプリらしい部分です。
この節では、次のようなことを学びます。
大きな背景画像を表示する
↓
画像の上に黒いグラデーションを重ねる
↓
ロゴや作品タイトルを重ねる
↓
PlayボタンとInfoボタンを配置する
↓
ボタンを押したときに別画面へ移動する
ここで特に大事になるのが、Stack と Positioned です。
この2つを使えるようになると、背景画像の上に文字やボタンを重ねるUIが作れるようになります。
Home画面の上部はどこで作っている?
今回のHome画面上部は、主にこのクラスで作っています。
class NetflixTopArea extends StatelessWidget {
const NetflixTopArea({
super.key,
required this.hero,
required this.selectedCategory,
required this.onTapMenu,
required this.onSelectCategory,
});
final MovieItem hero;
final HomeCategory selectedCategory;
final VoidCallback onTapMenu;
final ValueChanged<HomeCategory> onSelectCategory;
NetflixTopArea は、Home画面の一番上にある大きなビジュアル部分です。
ここには、次のような要素が入っています。
| 要素 | 内容 |
|---|---|
| 背景画像 | 作品の横長画像 |
| 黒いグラデーション | 文字を読みやすくするための暗い重ね色 |
| ヘッダー | メニュー、ロゴ、プロフィール画像、カテゴリタブ |
| 中央ロゴ | NETAFLIXロゴ |
| 作品タイトル | 大きく表示されるタイトル |
| 作品情報 | カテゴリ、シーズン、年齢制限 |
| ボタン | My List、Play、Info |
Home画面の第一印象を作っている、とても大事な部分です。
heroとは?
NetflixTopArea には、hero という名前の MovieItem が渡されています。
final MovieItem hero;
ここでいう hero は、Home画面の一番大きな背景に使う作品データのことです。
NetflixHomePage の中では、次のように決めています。
final items = filteredMovies.isEmpty ? movies : filteredMovies;
final hero = items.first;
これは、
表示対象の作品リストの一番最初の作品を、
Home画面上部のメイン作品として使う
という意味です。
たとえば、items.first が Squid Game なら、Home画面上部には Squid Game の背景画像やタイトルが表示されます。
SizedBoxで高さを決める
NetflixTopArea の最初の見た目は、次のようになっています。
@override
Widget build(BuildContext context) {
return SizedBox(
height: 610,
child: Stack(
children: [
...
],
),
);
}
ここでは、Home画面上部の高さを 610 にしています。
height: 610,
この数字が大きいほど、背景画像のエリアが縦に大きくなります。
スマホ画面のかなり大きな部分を使って、作品を見せるための指定です。
動画アプリ風のUIでは、最初に大きなビジュアルを見せることで、作品の世界観を伝えやすくなります。
Stackとは?
今回の一番大事なWidgetが Stack です。
child: Stack(
children: [
...
],
),
Stack は、Widgetを重ねて表示するときに使います。
普通の Column は、上から下に並べます。
ロゴ
↓
タイトル
↓
ボタン
普通の Row は、左から右に並べます。
アイコン 文字 ボタン
一方、Stack は重ねます。
背景画像
その上に黒いグラデーション
その上にヘッダー
その上にタイトルやボタン
今回のHome画面上部は、まさにこの形です。
背景画像を表示する
まず、背景画像を表示している部分を見てみましょう。
Positioned.fill(
child: Image.network(
hero.backdropUrl,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
ここでは、hero.backdropUrl の画像を画面いっぱいに表示しています。
backdropUrl は、作品データの中にある横長の背景画像URLです。
backdropUrl:
'https://image.tmdb.org/t/p/w1280/oaGvjB0DvdhXhOAuADfHb261ZHa.jpg',
この画像を、Image.network で読み込んでいます。
Image.networkとは?
Image.network は、インターネット上の画像を表示するためのWidgetです。
Image.network(
hero.backdropUrl,
)
今回の作品画像は、アプリの中に保存している画像ではなく、URLから読み込んでいます。
つまり、
作品データに画像URLを書く
↓
Image.networkでその画像を表示する
という流れです。
画像URLを変えると、Home画面の背景画像も変わります。
Positioned.fillとは?
背景画像は、Positioned.fill で囲まれています。
Positioned.fill(
child: Image.network(...),
),
Positioned.fill は、Stack の中で使います。
意味としては、
親のStackいっぱいに広げて表示する
です。
今回の場合は、SizedBox(height: 610) の中いっぱいに背景画像を広げています。
そのため、Home画面上部全体に大きな画像が表示されます。
BoxFit.coverとは?
画像には、次の指定があります。
fit: BoxFit.cover,
BoxFit.cover は、画像を枠いっぱいに表示するための指定です。
画像の縦横比を保ったまま、表示エリア全体を埋めるように拡大・縮小します。
そのため、画像の一部が少し切れることもあります。
ただし、背景画像として使う場合は、余白が出るよりも、画面全体にしっかり広がった方がきれいに見えます。
そのため、今回のようなHome画面の大きな背景画像には BoxFit.cover がよく合います。
| 指定 | 見え方 |
|---|---|
BoxFit.cover | 枠いっぱいに表示する。一部が切れることがある |
BoxFit.contain | 画像全体を表示する。余白が出ることがある |
BoxFit.fill | 枠に合わせて伸ばす。画像がゆがむことがある |
背景画像では、基本的に cover が使いやすいです。
黒いグラデーションを重ねる
背景画像の上には、黒いグラデーションを重ねています。
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
NetflixColors.black,
Colors.black.withValues(alpha: 0.92),
Colors.black.withValues(alpha: 0.28),
Colors.black.withValues(alpha: 0.78),
NetflixColors.black,
],
stops: const [0.0, 0.16, 0.38, 0.78, 1.0],
),
),
),
),
このグラデーションの目的は、背景画像を少し暗くして、上に乗せる文字やボタンを読みやすくすることです。
画像の上にそのまま白い文字を置くと、画像の明るい部分では文字が読みにくくなることがあります。
そこで、黒い半透明のグラデーションを重ねています。
なぜグラデーションが必要なの?
背景画像が明るいシーンだった場合、白い文字は見えにくくなります。
でも、画像の上に黒いレイヤーを重ねると、文字が読みやすくなります。
今回のグラデーションは、上と下をしっかり暗くして、中央は少し画像が見えるようにしています。
上:黒くする
中央:画像を見せる
下:文字やボタンがあるので暗くする
このように、グラデーションは見た目をかっこよくするだけではありません。
文字を読みやすくするためにも使います。
動画アプリ風のUIでは、とてもよく出てくる表現です。
colorsとstopsの意味
グラデーションでは、色の並びを colors で指定しています。
colors: [
NetflixColors.black,
Colors.black.withValues(alpha: 0.92),
Colors.black.withValues(alpha: 0.28),
Colors.black.withValues(alpha: 0.78),
NetflixColors.black,
],
alpha は透明度のようなものです。
| 値 | 見え方 |
|---|---|
1.0 | しっかり表示 |
0.92 | かなり黒い |
0.28 | 少しだけ黒い |
0.78 | しっかり暗い |
0.0 | 透明 |
そして、stops は色の位置を決めています。
stops: const [0.0, 0.16, 0.38, 0.78, 1.0],
ざっくり言うと、0.0 が一番上、1.0 が一番下です。
この設定によって、画面の上と下は暗く、中央は画像が見えやすくなっています。
ヘッダーを上に重ねる
次に、上部のヘッダーを重ねています。
Positioned(
left: 0,
right: 0,
top: 0,
child: NetflixHeader(
selectedCategory: selectedCategory,
onTapMenu: onTapMenu,
onSelectCategory: onSelectCategory,
),
),
Positioned は、Stack の中で位置を指定するために使います。
ここでは、
左端から0
右端から0
上から0
なので、画面の上いっぱいに NetflixHeader を配置しています。
NetflixHeader の中には、メニューアイコン、ロゴ、プロフィール画像、カテゴリタブがあります。
Home画面の上部に固定されているように見える部分です。
Positionedとは?
Positioned は、Stack の中で「どこに置くか」を指定するWidgetです。
たとえば、今回のコードでは、
top: 0,
left: 0,
right: 0,
と指定しています。
これは、
上にぴったり
左にぴったり
右にぴったり
という意味です。
別の場所では、下にタイトルやボタンを配置しています。
Positioned(
left: 18,
right: 18,
bottom: 34,
child: Column(
children: [
...
],
),
),
これは、
左に18px余白
右に18px余白
下から34px上に配置
という意味です。
Stack と Positioned を組み合わせると、背景画像の上に自由にUIを置けるようになります。
下部にロゴ・タイトル・ボタンを置く
Home画面上部の下の方には、ロゴ、作品タイトル、カテゴリ情報、ボタンを配置しています。
Positioned(
left: 18,
right: 18,
bottom: 34,
child: Column(
children: [
const NetflixWordLogo(height: 38),
const SizedBox(height: 18),
Text(
hero.title.toUpperCase(),
textAlign: TextAlign.center,
style: const TextStyle(
color: NetflixColors.white,
fontSize: 30,
fontWeight: FontWeight.w900,
letterSpacing: 2.4,
height: 1.05,
),
),
const SizedBox(height: 14),
Text(
'${hero.category} • ${hero.seasonLabel} • ${hero.rating}',
style: const TextStyle(
color: NetflixColors.white,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 18),
Row(
children: [
...
],
),
],
),
),
ここでも Positioned を使っています。
bottom: 34 と書くことで、背景画像エリアの下から少し上に配置しています。
つまり、画面の下の方にロゴ、タイトル、ボタンをまとめて置いているということです。
作品タイトルを大きく表示する
作品タイトルは、次のように表示しています。
Text(
hero.title.toUpperCase(),
textAlign: TextAlign.center,
style: const TextStyle(
color: NetflixColors.white,
fontSize: 30,
fontWeight: FontWeight.w900,
letterSpacing: 2.4,
height: 1.05,
),
),
ポイントは、hero.title.toUpperCase() です。
これは、作品タイトルを大文字にして表示する処理です。
たとえば、作品タイトルが、
Squid Game
だった場合、画面では次のように表示されます。
SQUID GAME
動画アプリ風の大きなタイトルでは、大文字にすると、見出しとして強く見えます。
TextStyleで文字の見た目を整える
タイトルの見た目は、TextStyle で細かく調整しています。
style: const TextStyle(
color: NetflixColors.white,
fontSize: 30,
fontWeight: FontWeight.w900,
letterSpacing: 2.4,
height: 1.05,
),
それぞれの意味は次の通りです。
| 指定 | 内容 |
|---|---|
color | 文字色を白にする |
fontSize | 文字サイズを30にする |
fontWeight | かなり太くする |
letterSpacing | 文字と文字の間を広げる |
height | 行の高さを少し詰める |
fontWeight: FontWeight.w900 は、かなり太い文字です。
作品タイトルを主役に見せたいので、ここでは強めの文字にしています。
作品情報を表示する
タイトルの下には、カテゴリ、シーズン、年齢制限を表示しています。
Text(
'${hero.category} • ${hero.seasonLabel} • ${hero.rating}',
style: const TextStyle(
color: NetflixColors.white,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
ここでは、文字の中に変数を埋め込んでいます。
たとえば、次のようなデータが入っていたとします。
category: 'TV Drama',
seasonLabel: 'Season 1',
rating: '18+',
その場合、画面には次のように表示されます。
TV Drama • Season 1 • 18+
- は区切り記号です。
こうすると、短いスペースで作品情報をすっきり見せられます。
ボタンを横に並べる
次に、My List、Play、Infoボタンを表示している部分です。
Row(
children: [
HeroActionButton(
icon: Icons.add,
label: 'My List',
onTap: () {},
),
const SizedBox(width: 12),
Expanded(
child: PlayButton(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) =>
YouTubePlayerPage(movie: hero),
),
);
},
),
),
const SizedBox(width: 12),
HeroActionButton(
icon: Icons.info_outline,
label: 'Info',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => MovieDetailPage(movie: hero),
),
);
},
),
],
),
ここでは Row を使って、3つのボタンを横に並べています。
My List | Play | Info
中央のPlayボタンだけは Expanded で包まれています。
そのため、左右の小さなボタンよりも横に広く表示されます。
My Listボタン
左側のボタンは、HeroActionButton を使っています。
HeroActionButton(
icon: Icons.add,
label: 'My List',
onTap: () {},
),
これは、アイコンと文字を縦に並べたシンプルなボタンです。
今のコードでは、onTap: () {} になっているので、押しても特に何も起きません。
ただし、あとからお気に入り保存などの機能を入れる場所として使えます。
今は「見た目だけ先に作っている」と考えると分かりやすいです。
Playボタン
中央のPlayボタンは、PlayButton という部品で作っています。
Expanded(
child: PlayButton(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) =>
YouTubePlayerPage(movie: hero),
),
);
},
),
),
このボタンを押すと、YouTubePlayerPage に移動します。
そのときに、movie: hero を渡しています。
つまり、
Home画面で表示しているメイン作品のデータを、
YouTube再生画面に渡す
ということです。
YouTubePlayerPage 側では、その作品データの youtubeVideoId を使って動画を再生します。
Navigator.pushとは?
ここで使っているのが、Navigator.of(context).push です。
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => YouTubePlayerPage(movie: hero),
),
);
これは、新しい画面へ移動するためのコードです。
流れとしてはこうです。
Playボタンを押す
↓
YouTubePlayerPageを開く
↓
heroの作品データを渡す
↓
その作品のYouTube動画が再生される
push は、今の画面の上に新しい画面を重ねるイメージです。
戻るボタンを押すと、前のHome画面に戻れます。
Infoボタン
右側のInfoボタンも、HeroActionButton で作っています。
HeroActionButton(
icon: Icons.info_outline,
label: 'Info',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => MovieDetailPage(movie: hero),
),
);
},
),
このボタンを押すと、作品詳細画面に移動します。
こちらも movie: hero を渡しています。
つまり、Home画面上部に出ている作品の詳細画面を開く流れです。
Infoボタンを押す
↓
MovieDetailPageを開く
↓
heroの作品データを渡す
↓
タイトル・説明文・カテゴリなどを表示する
HeroActionButtonを見てみよう
HeroActionButton の中身も確認しておきましょう。
class HeroActionButton extends StatelessWidget {
const HeroActionButton({
super.key,
required this.icon,
required this.label,
required this.onTap,
});
final IconData icon;
final String label;
final VoidCallback onTap;
このボタンは、アイコン、文字、タップ時の処理を受け取ります。
使う側では、次のように指定します。
HeroActionButton(
icon: Icons.info_outline,
label: 'Info',
onTap: () {
...
},
),
このように部品化しておくと、My ListボタンとInfoボタンのような似た見た目のボタンを簡単に使い回せます。
HeroActionButtonの見た目
中身はこのようになっています。
@override
Widget build(BuildContext context) {
return SizedBox(
width: 76,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Column(
children: [
Icon(icon, color: NetflixColors.white, size: 28),
const SizedBox(height: 5),
Text(
label,
style: const TextStyle(
color: NetflixColors.white,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
],
),
),
);
}
ここでは、アイコンと文字を Column で縦に並べています。
アイコン
↓
文字
また、GestureDetector を使ってタップできるようにしています。
PlayButtonを見てみよう
中央のPlayボタンは、白い横長ボタンです。
class PlayButton extends StatelessWidget {
const PlayButton({
super.key,
required this.onTap,
});
final VoidCallback onTap;
見た目は次のように作っています。
@override
Widget build(BuildContext context) {
return SizedBox(
height: 42,
child: ElevatedButton.icon(
onPressed: onTap,
style: ElevatedButton.styleFrom(
backgroundColor: NetflixColors.white,
foregroundColor: NetflixColors.black,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
icon: const Icon(Icons.play_arrow_rounded, size: 28),
label: const Text(
'Play',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
),
);
}
ElevatedButton.icon を使うと、アイコン付きのボタンを簡単に作れます。
ここでは、白い背景に黒い文字で、動画アプリらしいPlayボタンにしています。
Home画面らしさは「重ね方」で決まる
今回のHome画面上部は、次のように重なっています。
1. 背景画像
2. 黒いグラデーション
3. 上部ヘッダー
4. ロゴ
5. 作品タイトル
6. 作品情報
7. My List / Play / Info ボタン
この重ね方が、動画アプリ風の見た目を作っています。
特に、Stack と Positioned を使うことで、背景画像の上に自由にUIを置くことができます。
この考え方は、動画アプリだけでなく、旅行アプリ、飲食店アプリ、イベントアプリなどでも使えます。
観光地の写真の上に、地域名と予約ボタンを重ねる
レストランの写真の上に、店名とメニューを見るボタンを重ねる
イベントの写真の上に、開催日と参加ボタンを重ねる
画面をかっこよく見せたいときは、ただ並べるだけでなく、「重ねる」レイアウトも考えてみましょう。
まずカスタマイズしてみよう
今回は、Home画面上部の高さを変えてみましょう。
次のコードを探してください。
return SizedBox(
height: 610,
child: Stack(
この 610 を少し小さくしてみます。
return SizedBox(
height: 540,
child: Stack(
保存して、画面を確認してください。
Home画面上部のビジュアルエリアが少し低くなり、その下の作品一覧が早く見えるようになります。
逆に大きくしたい場合は、次のようにできます。
height: 680,
大きくすると迫力は出ますが、下の作品一覧が見えにくくなります。
UIでは、見た目の迫力と使いやすさのバランスが大切です。
タイトルの大きさをカスタマイズしてみよう
次に、作品タイトルの文字サイズを変えてみます。
次のコードを探してください。
fontSize: 30,
これを少し大きくします。
fontSize: 36,
保存して、Home画面を確認してください。
作品タイトルが大きくなります。
ただし、大きくしすぎると、長いタイトルが画面に収まりにくくなります。
タイトルが長い作品もあるので、画面全体のバランスを見ながら調整しましょう。
Playボタンの文字をカスタマイズしてみよう
Playボタンの表示文字を変えてみます。
PlayButton の中にある次のコードを探してください。
label: const Text(
'Play',
これを日本語にしてみます。
label: const Text(
'再生',
保存して、Home画面を確認してください。
中央のボタンが 再生 に変わっていれば成功です。
授業用のアプリとして分かりやすくしたい場合は、日本語にするのも良い方法です。
よくあるつまずきポイント
Q. 背景画像が表示されません。
まず、hero.backdropUrl に入っているURLが正しいか確認してください。
Image.network(
hero.backdropUrl,
fit: BoxFit.cover,
)
ブラウザで画像URLを開いて、画像が表示されるか確認すると分かりやすいです。
Q. 文字が読みにくいです。
背景画像の明るさによって、白文字が見づらくなることがあります。
その場合は、黒いグラデーションを少し強くします。
Colors.black.withValues(alpha: 0.28),
これを少し濃くします。
Colors.black.withValues(alpha: 0.45),
中央部分が少し暗くなり、文字が読みやすくなります。
Q. Playボタンを押しても動画が開きません。
この部分があるか確認してください。
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => YouTubePlayerPage(movie: hero),
),
);
また、hero.youtubeVideoId が正しく入っているかも確認しましょう。
チャレンジ
チャレンジ1:Home画面上部の高さを変えてみよう
次のコードを探してください。
height: 610,
これを次のように変えてみましょう。
height: 540,
Home画面の見え方がどう変わるか確認してください。
チャレンジ2:タイトルの文字を大きくしよう
次のコードを探してください。
fontSize: 30,
これを次のように変更します。
fontSize: 36,
作品タイトルが大きくなるか確認しましょう。
チャレンジ3:Playボタンを日本語にしよう
次のコードを探してください。
label: const Text(
'Play',
これを次のように変更します。
label: const Text(
'再生',
チャレンジ4:背景画像の暗さを調整しよう
次のコードを探してください。
Colors.black.withValues(alpha: 0.28),
これを次のように変更します。
Colors.black.withValues(alpha: 0.45),
背景画像が少し暗くなり、文字が読みやすくなるか確認してください。
チャレンジの答え
チャレンジ1の答え
変更前:
height: 610,
変更後:
height: 540,
Home画面上部のビジュアルエリアが少し低くなります。
チャレンジ2の答え
変更前:
fontSize: 30,
変更後:
fontSize: 36,
作品タイトルが大きく表示されます。
チャレンジ3の答え
変更前:
label: const Text(
'Play',
変更後:
label: const Text(
'再生',
中央のPlayボタンの文字が日本語になります。
チャレンジ4の答え
変更前:
Colors.black.withValues(alpha: 0.28),
変更後:
Colors.black.withValues(alpha: 0.45),
背景画像の中央部分が少し暗くなり、文字が読みやすくなります。
この節のまとめ
この節では、Home画面上部のUIを作る仕組みを学びました。
大切なポイントは次の通りです。
- Home画面上部は、
NetflixTopAreaで作っている。 heroは、Home画面のメイン作品として表示するMovieItem。Stackを使うと、背景画像の上に文字やボタンを重ねられる。Positioned.fillを使うと、背景画像をエリアいっぱいに広げられる。Image.networkは、URLから画像を読み込んで表示する。BoxFit.coverは、画像を枠いっぱいに表示する指定。- 黒いグラデーションを重ねると、文字が読みやすくなる。
Positionedを使うと、ヘッダーやボタンの位置を細かく指定できる。Navigator.pushを使うと、PlayボタンやInfoボタンから別画面へ移動できる。- ボタンやロゴを部品化しておくと、同じ見た目を使い回しやすい。
次のステップ
次の節では、背景画像の上に黒いグラデーションを重ねる表現を、もう少し詳しく見ていきます。
画像の上に文字を置くとき、どうすれば読みやすく、かっこよく見えるのかを学びます。
動画アプリ風のUIだけでなく、いろいろなアプリに使える考え方です。