Material Designとは?
Material Designとは、Googleが提唱している、デザインの方針を定める一連のガイドラインみたいなものです。
これに従ってアプリをデザインすると、美しくて使いやすいアプリをデザインすることができます。
FlutterのデザインやレイアウトのシステムはMaterial Designに準拠していますので、自分のアプリを作るときも極力それに逆らわず、流れに乗り、製作者の意図を汲んで、Material Designに準拠したアプリを作っていきたいものです。もっと細かくこだわりたい場合は別ですけれどもね。
Material DesignにおけるTheme
ざっくりリンク先のページを見ていただくと、次の4つを決めてアプリ全体に適用すると、統一感のある美しいアプリにできるという旨が書いてあります。
- Color
- Typography
- Shape
- Icons
一つずつ簡単に説明します。
Color
配色のことです。次の12個の色を決め、アプリ全体で使用すると良いです。
12個全部が別の色になっていなくてもいいです。
Material Designデフォルトの配色例がこれです。
Implementing your themeのColorの所より引用しました。
Typography
文字組みのことです。次の13個のスタイル(フォント・大きさ・太さ・色など)を決め、アプリ全体で使用すると良いです。
Material Designデフォルトのタイポグラフィ設定例がこれです。
Implementing your themeのTypographyの所より引用しました。
Shape
ボタンをはじめとする様々な要素・コンポーネントの縁の形です。
「直角にする」「丸くする」「左上だけ斜めに切り落とす」などが考えられます。
一つ方針を決めて、アプリ全体で使用すると良いです。
Icons
同じ目的のアイコンでも、「丸っこいもの」「角張ったもの」「中が塗られたもの」「塗られてないもの」など、いくつか種類があります。
これも一つ方針を決めて、アプリ全体で使用すると良いです。
FlutterにおけるTheme
FlutterにはThemeという仕組みがあり、これを指定するとアプリ全体に自動的に適用され、統一感のあるアプリを作ることができます。
ThemeData
という、一通りの設定を持ったクラスのインスタンスを、MaterialApp
かTheme
というWidgetに渡してWidgetツリーに組み込めば、その傘下のWidgetすべてに適用してくれます。
しかし、このThemeData
には大量の設定項目(プロパティ)があり、一つずつ設定するのはとても大変です。
こちらのリンク先に飛んだ瞬間、引数の多さに驚きます。
また、各プロパティにバラバラの色を設定することができてしまうので、統一感がなくなるリスクもあります。
FlutterのThemeにMaterial Designを適用する
ThemeData.from
コンストラクタ
ThemeData
クラスには、ThemeData.from
というコンストラクタがあります。
このコンストラクタには、colorScheme
とtextTheme
という引数を与えることができ、これがまさにMaterial DesignにおけるThemeのColorとTypographyに対応しているのです!
リンク先のドキュメントにも
This is the recommended method to theme your application.
と、オススメであるとハッキリ書いてあります!
ということで、これを使いましょう。
MaterialApp
のtheme
引数に与えて
MaterialApp(
theme: ThemeData.from(
colorScheme: 何かしらのColorScheme,
textTheme: 何かしらのTextTheme,
),
home: ScaffoldとかのWidget
)
という感じの構造にします。
Color
ColorScheme
クラスのコンストラクタは、まさにMaterial Designで規定されている12個の色を引数に取ります。
12個全て指定してもいいですが、下記のようにすると、基本はデフォルトの色を使いつつ、一部だけ異なる色を指定することができます。
この例では、primary
のみColors.red
に変え、あとはデフォルトの色を使うことになります。
ThemeData.from(colorScheme:ColorScheme.light(primary:Colors.red))
また、Widgetツリーの子孫から呼び出して読み取る場合もThemeData
から直接読み取るのではなく、一度ColorScheme
を経由するといいでしょう。
color: Theme.of(context).colorScheme.primary,
Typography
TextTheme
クラスは、まさにMaterial Designで規定されている13種類のスタイルを引数に取ります。
以前はそうなっていませんでしたが、最近変更されてMaterial Design準拠になりました(執筆日:2021年2月11日)。古い引数はdeprecatedになっています。その経緯により、一部、Material Designで規定されているものと名前が違います。
最新のMaterial Designに準拠したデフォルトのTextTheme
は
Typography.material2018().black
Typography.material2018().white
の2つです。明るい色を基調としたアプリの場合は文字色はblackにするため前者を、ダークテーマの場合は後者を選ぶといいでしょう。
これらを元に、色を変える場合はcopyWithを使ってこんな感じにしましょう。
Typography.material2018().black.copyWith(
headline1: TextStyle(color: Colors.brown),
subtitle1: TextStyle(color: Colors.orange),
bodyText1: TextStyle(color: Colors.pink),
...
)
GoogleFontsを併用する場合はこんな感じ。
GoogleFonts.abrilFatfaceTextTheme(Typography.material2018().black.copyWith(
headline1: TextStyle(color: Colors.brown),
...
))
なお、このTextTheme
を使う場合、フォントサイズや太さは変えるべきではないです。
根拠はこちらのページblack propertyに
This TextTheme should provide color but not geometry (font size, weight, etc). A text theme's geometry depends on the locale.
とある通り、各プロパティのTextStyleのフォントサイズや太さは、言語設定(Locale)に依存してFlutterが自動で設定するからです。
ScriptCategoryについて
Internationalizing Flutter appsに従って正しくローカリゼーション設定をした場合、Flutterが言語設定に従ってTextThemeの各TextStyleのフォントサイズや太さやを指定してくれます。
その設定はScriptCategory enumとして次の3種類が定義されています。
- englishLike
- 英語など
- dense
- 日本語など
- tall
- タイ語など
開発者である我々はこれを深く意識することなく、先程の
Typography.material2018().black
Typography.material2018().white
を使っておけば、Flutterが勝手に設定してくれます。
指定したい場合は
Typography.material2018().englishLike
などとすればよいです。
ShapeとIconsについて
ShapeとIconsについては、全体的なテーマを一括で指定する方法はなさそうです。
Shape
ThemeData
の引数CardTheme
やElevatedButtonTheme
などをそれぞれ指定するしかないと思います。ThemeData.from
コンストラクタにはこれらの引数はありませんので、.copyWith
を使って
ThemeData.from(
colorScheme: yourColorScheme,
textTheme: yourTextTheme,
).copyWith(
elevatedButtonTheme: yourElevatedButtonThemeData,
outlinedButtonTheme: yourOutlinedButtonTheme,
cardTheme: yourCardTheme,
dialogTheme: yourDialogTheme,
...
)
という感じです。面倒ですね。
Buttonにスタイルを適用する方法についてはこの記事がわかりやすいと思います。
FlutterのButton系Widgetが置き換えられて変わったところ
Icons
Icon
Widgetを使う度に、自分で決めた統一的な基準に従ってアイコンを選択しましょう。
IconThemeData classというものもありますが、これはcolor, opacity, sizeを指定するものなので、Material Designで言われているアイコンの形状に関するものとは別物です。
何がどこに適用されるか
ColorScheme
やTextTheme
を使って色やフォントを指定した場合、デフォルトではどこに適用されるのでしょう?
検証用アプリを作って検証してみましたのでその結果をご紹介します。なお、全てを列挙できているわけではないのでご了承ください。
下記のリストにないもの(ColorSchemeのprimaryVariant
やTextThemeのheadline1
など)は恐らくデフォルトで使われていないと思われますが、自分でそのプロパティをWidgetツリーの下部から指定すればもちろん使用可能です。
ColorScheme
- primary
- appBarの背景
- ElevatedButtonの色
- OutlinedButton, TextButtonのテキストの色
- BottomNavigationBarの選択されているもの
- ダイアログのテキストボタン
- secondary
- FloatingActionButtonの色
- スクロールが端までいった事を示すエフェクト(Androidのみ)
- surface
- Cardの色
- ライセンスページのロード完了後の背景色
- background
- Scaffoldのbodyの背景色
- ダイアログの背景色
- Drawerの背景色
- BottomNavigationBarの背景色
- ライセンスページのロード中の背景色
- error
- TextFormFieldのvalidation失敗時の色
- onPrimary
- ElevatedButtonのテキストの色
- AppBar内の選択中のタブを示す横棒
- onSecondary
- FloatingActionButtonのchildの色
TextTheme
- headline5
- AboutDialogやライセンスページのapplicationName
- headline6
- AlertDialogのtitle
- subtitle1
- ListTileのtitle
- AlertDialogのcontent
- AboutDialogのchildren
- ライセンスページのパッケージ名
- bodyText1
- DrawerHeaderのchild
- Drawer内のListTileのtitle
- bodyText2
- 通常のText
- Cardのchild
- AboutDialogやライセンスページのバージョン名
- caption
- ListTileのsubtitle
- BottomNavigationBarの選択されていないもの
- AboutDialogのapplicationLegalese
BottomNavigationBarの中身が、選択時はColorScheme
のprimaryColor
で、非選択時はTextTheme
のcaption
の色になるのは面白いと思いました。
通常の本文テキストがbodyText1
ではなくbodyText2
なのも面白い、というか罠ですね。
何も適用されない主な場所
- AppBar内の諸項目
- title
- leading
- actions
- bottomに配置するTabBar内のIconやText
AppBarの中では、背景色がprimary、選択中のタブを示す横棒がonPrimaryになる以外、どの色も適用されないようです。
AppBarにTextThemeを適用する方法については後述します。
- ListTile両端のアイコン
- 非選択時のTextFormFieldの下線やラベルの色
- disabled状態のボタン
これらは灰色のままです。
検証用アプリの紹介
今回の検証にはこちらのアプリを使用しました。
GitHubからcloneして起動すれば使えるはずです。
このアプリの各所に指定されている色と、ソースコードで指定している色を見比べてみてください。
BottomNavigationBarからcolorSchemeというボタンを押すと、colorSchemeだけをいろいろ指定したモードになります。
BottomNavigationBarからtextThemeというボタンを押すと、textThemeだけをいろいろ指定したモードになります。この時のcolorSchemeには白・黒・グレーだけを使用したColorSchemeを指定しているので、色がついているところはtextTheme由来だということが明確になります。
なお、アイコンの選択は完全にテキトーです。
また、アプリの目的上できるだけバラバラに色を指定していますので、デザインとして美しいかどうかはお察しください。特にtextThemeだけ指定してある方、ひどいですね。
それから、Localeを変化させることでScriptCategoryを3通り(englishLike
, dense
, tall
)変化させることができるようにしてありますが、その際表示する文字列は変化しません(英語から日本語やタイ語になったりはしません)。同じ文字列で比較することでScriptCategoryによる違いを見るためです。
ただし、Flutterデフォルトの文字列が使われている所(AboutDialogのライセンス表示ボタンなど)は言語が変化します。
Drawerを開いて、AboutDialogやライセンスページも見てみてくださいね。
じゃあAppBarにTextThemeを適用する方法は?
最後に、AppBarにTextThemeを適用する方法をご紹介します。
良くない方法
「そんなん、MaterialAppのtheme
引数に渡すThemeDataのappBarTheme
に適当なAppThemeData
を渡すだけじゃん」と思うかもしれません。
その通りなのですが、そこに罠があります。
AppBarのtitleテキストのフォントを、GoogleFontを使って変えたいとします。
先程のようにThemeData.from
コンストラクタを使ってThemeDataを作った後、.copyWith
でappBarTheme
を上書きしてみます。
final myThemeData = ThemeData.from(
colorScheme: ColorScheme.light(), // 好きなColorSchemeでOK
textTheme: Typography.material2018().black, // 好きなTextThemeでOK
).copyWith(
appBarTheme: AppBarTheme(textTheme: GoogleFonts.pacificoTextTheme()));
結果はこんな感じ。
確かにフォントは変わっていますが、なんだか小さいです。
色も黒くて見にくいし、左のハンバーガーメニューと色が違っていて変です。
これは、もともと使われていたAppBar用のTextThemeを完全無視して、新たに生成したTextThemeを渡してしまったせいです。
解説
そもそもどうしてThemeData.from
コンストラクタに渡したTextThemeは、AppBarに適用されないのでしょうか?
実は、ThemeData.from
コンストラクタに渡したTextThemeはそのままThemeDataのtextTheme
プロパティに渡されるのですが、
一方で、AppBarはThemeDataのprimaryTextTheme
プロパティを読みに行くからです。
ではじゃあprimaryTextTheme
にもtextTheme
プロパティと同じTextThemeを渡せばいいかと言うと、そういうわけでもない。
primaryTextTheme
は、primaryに設定した色の上に表示した時に見えやすい色を設定する必要があります。単にtextTheme
プロパティと同じにしてしまうと、たいへん見づらい結果を招くことがあるのです。
ちなみに、iconTheme
、primaryIconTheme
というプロパティもあり、同様の構造になっています。
良い方法
ということで、もともとprimaryTextTheme
に設定されているTextThemeを可能な限り活かしながら、最低限のフォント(と、ついでに色)を変更することにします。
primaryIconTheme
も同様にします。
このようにすると良いでしょう。
まずThemeData.from
を使ってThemeDataをつくります。
final _tempThemeData = ThemeData.from(
colorScheme: ColorScheme.light(), // 好きなColorSchemeでOK
textTheme: Typography.material2018().black, // 好きなTextThemeでOK
);
これを元に、primaryTextTheme
を、もともと設定されていたものを最大限残しながら部分的に変更します。(iconThemeも同様)
final themeData = _tempThemeData.copyWith(
primaryTextTheme:
GoogleFonts.pacificoTextTheme(_tempThemeData.primaryTextTheme).apply(
bodyColor: Colors.yellow,
),
primaryIconTheme:
_tempThemeData.primaryIconTheme.copyWith(color: Colors.yellow),
);
GoogleFontsではなく手元のフォントファイルを指定する場合はこう。
final themeData2 = _tempThemeData.copyWith(
primaryTextTheme: _tempThemeData.primaryTextTheme.apply(
fontFamily: 'DancingScript',
bodyColor: Colors.yellow,
),
primaryIconTheme:
_tempThemeData.primaryIconTheme.copyWith(color: Colors.yellow),
);
これでうまくいきます。
大きさも適切で、色設定もフォント設定もうまくいきましたね。
「ハンバーガーメニューは元の色でいいわ!」という場合はprimaryIconTheme
プロパティの設定をしなければいいです。
ちなみにですが、AppBarのtitleにはprimaryTextTheme
に渡したTextThemeのheadline6
プロパティにあるTextStyleが適用されます。へ〜。
補足
ThemeDataのappBarTheme
プロパティにAppBarThemeを渡しても良かったのですが、primaryTextTheme
プロパティを設定しておくほうがより全体に包括的に設定できるので良いと思います。
具体的には、CircleAvatarやFlexibleSpaceBar、SearchPageなどを使った場合に違いが出そうです(ソースコードより推察。未検証)。
おわり
おわりです。
用意されている機能を極力そのまま利用して、統一感のあるアプリを作っていきましょう!