Flutter の AppBar で遊んだら楽しかったよ

19 min read読了の目安(約17200字

こんにちはこんばんわ、すぎっと٩( ᐛ )وです
今日のテーマは AppBar です。
実用的な話題というより、遊び感覚でいろいろやったよっていう話になります。

AppBar を使っていますか?

Flutter に限らずですが、 AppBar って実はちょっと扱いが難しいパーツだと思っています。 Flutter が提供する AppBar は Material Design のガイドラインに即して作られていますので、それ自体はとてもよくできており、多くのアプリで採用されていることも納得の Widget です。

とりあえずベーシックな AppBar をみてみましょう。

basic_appbar

Flutter を一度でもビルドしたことがあるなら、よく見慣れたものだと思います。
では、この AppBar は どんな役割があるのでしょうか

AppBar はどうあるべきかが Material Design ガイドラインに以下のように記載されています。

The top app bar provides content and actions related to the current screen. It’s used for branding, screen titles, navigation, and actions.

https://material.io/components/app-bars-top

アプリのメイン画面に表示されている内容に即して情報を表示したり、ユーザー操作を提供したりするもの、といったところでしょうか。このことから分かる通り、アプリの主役というわけではありません。表示中のメインコンテンツを適切にユーザーに提供するために使用する補助的な要素だと考えています。

あなたのアプリに AppBar は必要ですか?

改めてこれについて考えてみても良いのではないでしょうか。まず、「本当に必要かどうか」考えてみてください。そのAppBarは どんな目的で そこにあるのでしょう。

😎 ...(今いるページがどこなのか、テキストでお知らせするためにあります
🙄 ...(遷移後のサブページならそれが良いと思いますが、メインページはどうでしょう? もし BottomNavigationBar を使用しているなら、そちらを見れば十分ではないですか?
😎 ...(ドロワーを出すためのハンバーガーメニューを置きたいんですよ
🙄 ...(では、中央の Title 部は本当はいらない・・・?

色々なケースがあると思いますが、例えば(↑)のようなやりとりを頭の中で巡らせてみてください。その結果、やはり AppBar は必要だ、という結論になったなら、ぜひ以降の説明を見て、AppBar でこういう表現ができるんだというのを知っていただき、よりよい AppBar を作ってみてください。

AppBar と Body の背景色を近い色で揃えてみよう

AppBar の色は Theme では AppBarTheme.backgroundColor に定義されています。これはデフォルトでは ColorScheme.primary が適用されますので、 MaterialApp の Theme で primarySwatch を変更したら変わるんだ、という風に覚えている方が多いのではないかと思います。正確には、「AppBar で barkgroundColor を指定していない」 かつ、 「MaterialApp の Theme でも AppBarTheme を指定していない」 場合、デフォルトの primary が適用されてます。この辺りはきちんとドキュメントを読むと記載があります。 これに関して詳しく解説した記事を書きましたので参考にしてみてください。

https://zenn.dev/sugitlab/articles/bef3a05963680a
theme: ThemeData(
  primarySwatch: Colors.cyan,
),

例えばこのように primarySwatch に cyan カラーを指定します。するとこうなります。
appbar_primary_cyan

AppBar は基本的には画面の上部に常に存在します。そのため、AppBarを有効に活用できていない場合、ユーザーによっては「画面の有効なスペースを奪っている邪魔な部分」というふうに捕らえてしまうこともあるかもしれません。例えば、ディスプレイのベゼルが分厚いのって、嫌ですよね? もしユーザーから "不要な部分" と思われてしまったなら、 そのAppBar は分厚いベゼルになります。悲しいですね。そんな時は試しに背景色と AppBar の色を揃えてみてください。

実際に見てみましょう。

white_appbar

真っ白にすると iOS 感がでますね。これについては以前の記事で説明しています。
なお、これは Material Design を iOS 風にしたいわけではありません。それなら初めから Cupertino Widget を使えば良いですからね。この目的は Bodyとの境界を曖昧にすることで、全体の一体感を出すこと です。

境界を曖昧にする、という意味では AppBar の Elevation を調整してみると面白いです。

  • elevation:0
    elevation_zero_appbar

  • elevation:2
    elevation_two_appbar

  • elevation:10
    elevation_ten_appbar

例えば Google Ads アプリも似たような表現をしていますね[1]

google_ads_ui

全体を白基調にしてあげるメリットとして、

  • ダークモード対応が分かりやすいこと
  • アクセントカラーが目立ちやすいこと

が挙げられます。アプリとしては大事なところにはアクセントカラーを使いたいものですが、AppBar でその色を使用していたり、別の色を使用している場合、本当に伝えたい部分のメッセージが弱まってしまうかもしれません。

もちろん、白基調にしなくても効果的な AppBar を作ることは可能です。早速、別のカラーでもみてみましょう。例えばちょっと取り扱いが難しそうな強めのカラー (Colors.purple) を使用してみました。メインコンテンツが何もないとイメージしずらいので、カードを1つ置いてみました。

theme: ThemeData(
  primarySwatch: Colors.purple,
  scaffoldBackgroundColor: Colors.purple[600],
),

purple_appbar_and_body

AppBar と Scaffold の背景色に同系色を指定してあげることで、 メインのコンテンツがグッと引き立つ ことがわかると思います。これはアプリのテーマカラーを効果的に使用する手法とも言えます。

これは Flutter Showcase にある Nubank の例を参考にしました。

https://flutter.dev/showcase

さて、背景色に同系色を使用するとメインのコンテンツが引き立つのは分かりましたが、実はまだ少しイマイチかな、とも思います。私はベタ塗りのカラーが広い範囲にあると、少し目にうるさく感じてしまうタイプです。

そこで、AppBar と Body の境目にハイライトが入るような雰囲気でグラデーションをかけてみます。すると先ほどよりは目に優しいというか、いい意味で印象的な雰囲気になったのではないでしょうか。

purple_appbar_and_body_with_gradient

グラデーションは Body を Container Widget でラップして BoxDecoration を適用することで実現しています。

body: Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      begin: Alignment.bottomCenter,
      end: Alignment.topCenter,
      colors: [Colors.purple[900], Colors.purple[700], Colors.purple[500]],
    ),
  ),
  child: MyWidget(),
),

背景に強い色が来るパターンについても説明しましたが、この手法はアプリ全体の雰囲気をガラッと変えてしまいますし、アプリのイメージそのものになるほどに強い表現だと思います。使い所を見極めて適切に使用することで、印象的なアプリになるのではないでしょうか。

AppBar をちょっとだけ透かしてみてはいかがですか?

AppBar はメインのコンテンツとの一体感が大事、というお話をしてきました。一体感を出すためのまた別のアプローチとして、 AppBar を透かしてみるのも手かもしれません。「AppBarはメインのコンテンツを邪魔するものではないんだよ」 ということを表現することができます。

アプリのメインコンテンツがたくさんあり、スクロールが必須なアプリ (LINEなど) では採用されていたりしますね。早速完成したものを見てみましょう。

transparent_appbar_gif

AppBarを透かすにはシンプルに透過性のある色を背景色に指定してあげれば大丈夫です。以下の例では透過色(色なし)を指定していますが、例えば Colors.green.withOpacity(0.5) などとすれば、透過したグリーンの AppBar にすることができます。

AppBar に elevation を設定していると、不要なシャドウが加わり、透過しているのに影がある状態になります。透過させるときは elevation を 0 にしましょう。

しかし、これだけでは単に背景色のないAppBarになっているだけで、 透過している感 が出ません。
これはなぜかというと、 メインのコンテンツのスクロール終了位置が AppBar の下部にある からです。

Scaffold Widget では、 AppBar がある場合の Body の 描画可能な領域 は AppBar より下である、というルールがデフォルトになっています。したがって、どれだけスクロールしても AppBar の下部に到達した時点で描画されなくなってしまいます。

transparent_failed

この描画の制限を取り払うための設定が Scaffold Widget には用意されています。それが、 extendBodyBehindAppBar です。

https://api.flutter.dev/flutter/material/Scaffold/extendBodyBehindAppBar.html
home: Scaffold(
        appBar: AppBar(
          leading: IconButton(icon: Icon(Icons.menu), onPressed: () {}),
          title: Text('HelloWorldApp'),
          centerTitle: true,
          actions: [
            IconButton(icon: Icon(Icons.search), onPressed: () {}),
            IconButton(icon: Icon(Icons.more_vert), onPressed: () {}),
          ],
          elevation: 0,
          backgroundColor: Colors.transparent,
        ),
        extendBodyBehindAppBar: true, // <--- ここ
	body: SingleChildScrollView(
	  // 省略
	),
     ),

この設定をすることにより、 Body の描画可能な領域が AppBar を無視して設定されます。そのため、デバイスの上端まで描画可能になり、透過色の AppBar の背後にコンテンツが入るようになります。

例えば グリーン + 透過 (Colors.green.withOpacity(0.3))なら、このような見た目になります。
green_transparent_appbar

一点、注意すべき点があります。 extendBodyBehindAppBar を true にすると初期配置の最上端がデバイス上部になります。従って、bodyの Widget のレイアウトが上にシフトします。つまり、本来は AppBar の下から始まるメインの領域が、デバイスの上端から始まるレイアウトになります。メインコンテンツの配置は初めは AppBar の下部にあって、スクロールした時だけ AppBar の背後に入るようにしたいというケースがほとんどたと思います。その場合はスクロールコンテンツの一番上に SizedBox Widget などを使って、スペースを入れてください。スペースの高さについては、 AppBar の toolBarHeight などを基準に計算してください。

AppBar を写真やイラストで作る

印象的なアプリにしたい場合、AppBarの背景にイラストや写真を使ってみてはいかがでしょうか。例えば、観光案内のアプリならその観光地の代表的な写真を使用したり、学校などのコミュニティで使用するアプリならそのコミュニティの写真、イベントのアプリならそのイベントのイメージ画像などです。

アプリを立ち上げた途端、その世界に飛び込んだような気持ちになって欲しいときに使うと効果的かもしれませんね。

では早速書き方ですが、AppBar の flexibleSpace を使います。

https://api.flutter.dev/flutter/material/AppBar/flexibleSpace.html
appBar: AppBar(
          leading: IconButton(icon: Icon(Icons.menu), onPressed: () {}),
          title: Text('Welcom to Tokyo'),
          centerTitle: true,
          actions: [
            IconButton(icon: Icon(Icons.search), onPressed: () {}),
          ],
          elevation: 1,
          flexibleSpace: Image.network( // <-- ここで指定します。
            'https://images.unsplash.com/photo-1513407030348-c983a97b98d8?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1352&q=80',
            fit: BoxFit.cover,
          ),
          toolbarHeight: 100, // <-- ここで高さを指定してあげるとまた雰囲気が変わります
          backgroundColor: Colors.transparent,
        ),

写真は unsplash から拝借しました。写真の見せ方に応じて、 toolbarHeight を調整してみてもいいかもしれませんね。

photo_appbar

東京の観光がしたくなるような AppBar になりましたね!! また自由に旅行ができる日々が待ち遠しいですね。

なお、今回は Image.network() を使用しましたが、画像をインターネットから探してくる場合は少し工夫したほうが良いです。それは、キャッシュを使うことです。ウィジェットの再描画がおきても、画像の再取得は不要なケースがほとんどです。キャッシュを適切に活用し、パフォーマンスの低下を避けましょう。以下のパッケージを使用すると簡単に実現することができます。

https://pub.dev/packages/cached_network_image

AppBar をスクロールに合わせて隠す

手元のアプリをいくつか触ってみていただきたいのですが、多くのアプリではスクロールに合わせてAppBarを隠す(または小さくする)という実装がされていることがわかると思います。これは、メインのコンテンツが大きなリストから構成されておりスクロールが必要な場合に採用されることが多い手法です。

このような表現は Material Design のガイドラインの中でも紹介されており、一般的な方法であると言えます。

https://material.io/components/app-bars-top#behavior

appbar_scroll_behavior

Flutter にはこれを実現する Widget が用意されています。 SliverAppBar です。 SliverAppbar と組み合わせて使用することが多いその他の Widget も総称して、 Sliver と呼ばれています。

https://api.flutter.dev/flutter/material/SliverAppBar-class.html

SliverAppBar Widget は AppBar という名前がついていますが、 Scaffold の AppBar として使用することはできません。 プロパティを見るとわかりますが、 Scaffold の AppBar にはPreferedSizeWidget を使用する必要があります。

preferedsizewidget

しかし、 SliverAppBar は普通の Widget です。これを踏まえると、 スクロールに合わせて隠すような対応をしたい場合は、Scaffold の appBar には何も指定せず、 body で全てを構成する必要がある ということになります。また、 SliverAppBar は普通の Widget ですと言いましたが、実は Sliver系統のWidget は Sliver系統のWidgetと一緒に使う というルールがあります。それは、 SliverWidget を使用する場合のスクロールの挙動を実装している CustomScrollView がもつ制約です。これはどういうことかというと、 SliverAppBar の下に何も処置せずいつもの Container ウィジェットを置くことはできないということです。おきたい場合は SliverToBoxAdapter を使うなどの工夫が必要です。順に見ていきましょう。

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

上記 API ドキュメントの冒頭に以下の説明があります。

A ScrollView that creates custom scroll effects using slivers.

大事なところは using slivers ですね。 Sliver 指定です。

では、ここで代表的な Sliver 系統の Widget をいくつか紹介します。

  • SliverAppBar
    • シュッと隠れる AppBar です。
  • SliverList
    • ListView です。
  • SliverGrid
    • GridView です。縦×横のグリッドですね。
  • SliverToBoxAdapter
    • Sliver の Widget の中に普通の(?)Widget を紛れ込ませるときに使います。

主に使用するのはこれらですね。加えて、以下の Widget を組み合わせることでより効果的な演出を実現することができます。

  • SliverPadding
  • SliverSafeArea
  • SliverOpacity
  • SliverAnimatedList

では早速 SliverAppBar + SliverList でメイン画面を作ってみましょう。

まずは何はともあれ CustomScrollView を使いましょう。

home: Scaffold(
  body: CustomScrollView(
    slivers: [],
  ),
),

CustomScrollView にはスクロールの動作に関する設定を調整するためのたくさんのプロパティが用意されていますが、使い始めは基本的に触らなくても大丈夫です。そうなると手を加えるところは slivers だけということになります。 sliver"s" となっていることからわかるように、ここには Sliver 系統の Widget を並べて使用します。 Column Widget と同じ感覚で使えば大丈夫です。

では早速 slivers として SliverAppBar と SliverList を実装してみます。

body: CustomScrollView(
  slivers: [
    SliverAppBar(
      leading: IconButton(icon: Icon(Icons.menu), onPressed: () {}),
      title: Text('Hello Sliver World'),
      actions: [
	IconButton(icon: Icon(Icons.search), onPressed: () {}),
      ],
    ),
    SliverList(
      delegate: SliverChildListDelegate(
	[
	  MyWidget(1),
	  MyWidget(2),
	  // .... ご自由に
	],
      ),
    ),
  ],
),

これを実行するとこのように表示されます。スクロールするとこんな感じですね。

これで効果的な SliverAppBar が完成しました。SliverAppBar はこのベーシックなところからの調整がとても楽しいです。

  • collapsedHeight
  • expandedHeight
  • flexibleSpace

この3つをうまく使うと、先に紹介した AppBarを写真やイラストで作る の発展形を作ることができます。

それがこちらです。

印象的な写真を使ってファーストインプレッションをいい感じにしつつ、いざアプリのコンテンツを見るときには潔く隠れてくれます。なんと素晴らしいことでしょうか。

今回は SliverList を使用しましたが、 SliverGrid や SliverToBoxAdapterもよく使います。Gridは言わずもがなグリッドですが、SliverToBoxAdapterはそのchildに普通の(?)Widgetを持つことができるので便利です。

AppBar がいい感じにカーブしているように見える工夫

AppBarの形状は基本的に長方形のものをよく見かけますが、実は少しアレンジを加えることができます。いくつか例を示していこうと思います。

round_appbar

AppBar の角を丸めてみました。実装は以下の通りです。

appBar: AppBar(
  leading: IconButton(icon: Icon(Icons.menu), onPressed: () {}),
  title: Text('Hello World', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
  shape: RoundedRectangleBorder( // <--- ここ
    borderRadius: BorderRadius.only(
      bottomLeft: Radius.circular(30),
      bottomRight: Radius.circular(30),
    ),
  ),
),

AppBarの形状を変えるために、 shape プロパティを使用しています。 shape には RoundedRectangleBorder を指定しました。これは、角をとった長方形のような境界を作ることができるものです。角丸ボタンみたいなイメージの形状です。

角の丸みを指定するのが borderRadius です。 BorderRadius は "どの角" に "どんなカーブ" を適用するのかを決めるものです。今回は画面上部はスマホの形状によって見え隠れするので丸め指定をせず、左右の下部のみ丸めてみました。Radius.circular() というのは、角を円形にくりぬくものです。中の数字を大きくするほど、よりカーブが強くなります。

いざ作ってみましたが、「へ〜こんなことできるんだね〜」 という感想ですね。正直、ちょっとデコってみたかっただけで 全然いい感じではない きがします。しかし、こういったカーブって上手く使えばいい感じになりそうな気もします。そこで、ちょっとゴリゴリした実装にはなりますが、変わったテイストの例を作ってみました。

home: Scaffold(
        appBar: AppBar(
          leading: IconButton(icon: Icon(Icons.menu), onPressed: () {}),
          title: Text('Hello World', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.only(
              bottomRight: Radius.elliptical(90, 30),
            ),
          ),
          elevation: 0,
        ),
        body: Align(
          alignment: Alignment.topCenter,
          child: Stack(
            children: [
              Container(
                height: 500,
                color: Theme.of(context).primaryColor,
              ),
              Container(
                height: 500,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(200),
	          ),
                  color: Theme.of(context).scaffoldBackgroundColor,
                ),
              ),
	      // カードの部分の実装
            ],
          ),
        ),
      ),

これを実行するとこうなります!

curve_appbar_to_body

AppBar から Body に向かって緩やかにカーブが描かれていますね。これにどんな意味があるのかは分からないですけど。 なんかおしゃれなんじゃないですかね。練習には良いかもしれません。

ここで使用しているテクニックが Stack です。

Stack は複数のウィジェットを重ねて表示するためのレイアウトウィジェットです。今回は、まず下地として以下のシンプルな Container を配置しています。

Container(
  height: 500,
  color: Theme.of(context).primaryColor,
),

高さはカーブが終わるかな〜という高さを何となくで指定しています。色は AppBar と同じ色にしたかったので、とりあえず primaryColor にしています。ここは AppBar の色をどのように指定しているのかによって変更してください。

そして、この色付きボックスの上に以下のウィジェットを配置します。

Container(
  height: 500,
  decoration: BoxDecoration(
    borderRadius: BorderRadius.only(
      topLeft: Radius.circular(200),
    ),
    color: Theme.of(context).scaffoldBackgroundColor,
  ),
),

こちらは先程の下地と同じ高さで、左上の角を大きくカーブさせています。さらに、色を scaffoldBackgroundColor にすることで、高さ 500 を超えた残りの下部エリアとも同じ色になり、その場に溶け込んでいるのです。

メインのコンテンツはこの2つのコンテナをStackしたさらに上側に乗せる形で配置してあげればOKです。

ではさらにもうひとつ、先の例で AppBar の色と Body の色を合わせつつ、白基調の Container を置くことで効果的に見せる方法を紹介しましたが、同じ手法で あたかも AppBar がカーブしているような 雰囲気を作ることができます。

home: Scaffold(
        appBar: AppBar(
          leading: IconButton(icon: Icon(Icons.menu), onPressed: () {}),
          title: Text('Hello World', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.only(
              bottomRight: Radius.elliptical(90, 30),
            ),
          ),
          elevation: 0,
        ),
        body: Stack(
          children: [
            Align(
              alignment: Alignment.bottomCenter,
              child: Container(
                height: 600,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(60),
                    topRight: Radius.circular(60),
                  ),
                  color: Colors.grey[100],
                ),
              ),
            ),
            // カード部分の実装
          ],
        ),
      ),

これを実行するとこうなります。

curve_body

これは実はとてもシンプルにできています。

  • body の背景を AppBar と同じ色にする
  • AppBar の elevation をゼロにする
  • 白基調のアイテムを bottom 基準で置く
  • メインのコンテンツはこれらの上に Stack して置く

これだけです。Stack って楽しいですね。

この例を本番採用するのはなかなか勇気のいることだと思いますが、効果的に使うことができれば、「おっ」と目を引くようなものになるのかもしれませんね。

もっとゴリゴリうねうねさせたいという方は、ClipPathを試してみてください。
https://api.flutter.dev/flutter/widgets/ClipPath-class.html

便利なパッケージはどんどん使いましょう

Flutter のパッケージを探すには pub.dev で検索するのが一番早いです。

https://pub.dev/

検索欄に appbar などと入力し、検索してみてください。たくさんのパッケージがありますので、みているだけでも楽しいですし、多くのパッケージは GitHub のリポジトリが公開されていますので、そこで実装を見てみるととても勉強になりますよ。

では、ここまで紹介したものにはなかったタイプのものとして、検索バーを紹介します。たくさんのデータから検索したいケースというのは多くのアプリで求められるものです。自分でイチから実装するにはちょっと手間がかかってしまうものでもあるため、パッケージに頼ってみても良いのかもしれません。

https://pub.dev/packages/flutter_search_bar

https://pub.dev/packages/floating_search_bar

https://pub.dev/packages/material_floating_search_bar

おしまい

AppBarに関していろいろ遊んでみました。
楽しかったですね!!
また新しい面白い見せ方を見つけたら追記しようと思います。

なお、ここで紹介した色々なAppBarについてはこちらで公開していますので、よければ参考にしてください。

https://github.com/sugitlab/flutter_ui_cookbook/tree/main/lib/appbar

AppBarまわりでお困りの方の一助になれば幸いです。

すぎっと٩( ᐛ )و

脚注
  1. Flutter製アプリについてはShowcaseを見ると良いです -> https://flutter.dev/showcase ↩︎