こども画面を「シール」の語彙に寄せる — 書体・配色・完了表現の設計
「もっとこども向けに、見た目もフォントもポップにしたほうがいい」。こども画面の刷新は、実際にアプリを使っている妻からのこの一言が出発点でした。
当時のこども画面は、丸ゴシック系の書体を選んでいながら、細字・小さめのサイズ・淡い輪郭で描いていました。部品のひとつひとつは可愛いのに、画面全体としてはぼやけて見える。指摘は「可愛くない」ではなく こども向けに見えない でした。
「こども向けに見える」を実装できる言葉に翻訳するところから始めました。決めたのは、画面上の要素を ひとつの視覚語彙に寄せる ことです。以下はその過程で決めたことです。
語彙を1つ決める
選んだ語彙は「シール」です。淡い地・地に対して濃い枠・厚みを出す影、の3点セットで、角丸と枠幅はおとな画面より一段強くします。やること行、連続達成のバッジ、こども画面のボタンを、すべてこの規格に揃えました。
語彙を1つに決めると、部品を足すときに迷いません。新しい要素が出てきたら「シールとして描けるか」だけを見ればよく、画面ごとに雰囲気が分岐しません。
ここで効いたのは、共用している部品にロール判定を持たせない ことです。連続達成のバッジはおとな画面でも使っています。部品の中でロールを見ると、後から例外が出た瞬間に条件が増えます。
// 却下した案: 部品の中でロールを見る
class StreakBadge extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isChild = ref.watch(currentRoleProvider) == Role.child;
// 部品がロールを知ってしまう。例外的な使い方が増えるたびに条件が増える
// 採用: 見た目は呼び出し元が決める
StreakIndicator(days: days, sticker: true) // こども画面
StreakIndicator(days: days) // おとな画面
実際にこの例外はすぐ現れました。同じ 2.0.0 で「おとなの端末に置くこども用の画面」を作ったため、見た目はこども・権限はおとな という組み合わせが必要になっています。見た目を呼び出し元が決める形なら、部品は書き換えずに済みます。
完了を色で示さない
この刷新では、やることに4色から色を選べるようにしました。こども自身が「宿題は青」「おてつだいは緑」と決められます。
すると、完了表現に色を使えなくなります。こどもが選んだ色と状態表示が同じチャンネルを取り合うためです。そこで完了は 影を消して沈ませ、スタンプを載せ、文字を薄くする で表しました。地の色は変えません。
// 完了=押し込まれて沈む。こどもが選んだ地の色は変えない
final sunk = isChild && task.isDone;
Padding(
// 影のぶんの余白を上下で入れ替える。沈んでも行の総高は変わらない
padding: EdgeInsets.only(
top: sunk ? sinkOffset : 0,
bottom: sunk ? 0 : sinkOffset,
),
child: DecoratedBox(
decoration: BoxDecoration(
color: fillOf(task.color),
border: Border.all(color: borderOf(task.color), width: 2.5),
boxShadow: sunk ? null : [stickerShadow], // 影を消すことで沈んで見せる
),
余白を入れ替えているのは、1件完了するたびに行の高さが変わって下の行が動くのを避けるためです。沈む演出は「見た目が動く」ことに意味があるので、レイアウトまで動いてはいけません。
副産物として、完了判定が色に依存しなくなりました。状態は形と位置で示し、色は装飾に使う と決めると、色覚特性の影響も自動的に外れます。
大きさが変わる場所は、器を先に決める
行末のマークは、完了スタンプと未完了の丸で大きさが違います。素直に並べると、完了したかどうかで左隣にある読み上げアイコンの位置が横にずれます。
固定サイズの枠を置いて、その中で中央寄せにしました。大きさの違うものが交代で入る場所は、中身ではなく器のサイズを決める ということです。
地の色がブランドなら、文字側で解く
配色は Markdown の frontmatter をトークンの単一情報源にしていて、そこから Flutter のテーマを生成し、lint がコントラスト比を検証します。その lint が指摘していた組み合わせが1つありました。ブランドのオレンジに白文字はコントラスト比 2.45:1 で、大きい文字の基準である 3.0 を下回ります。
検証は自動化してあるので、指摘は把握できます。ただし直し方は機械的には決まりません。地を変えるか、上に載る文字を変えるかは、ブランドの判断です。
刷新のタイミングで、地は変えず、文字を濃い茶にして 5.33:1 にしました。ブランドカラーは地の色として使い続けたいからです。おとな画面の主要ボタンも同じ文字色になりますが、そちらを白のままにする理由がないので揃えました。
やることの4色も同じ考え方で決めています。地は淡く、枠は地に対して 3:1 以上。枠は装飾ではなく、行の輪郭=タップ領域を規定する情報 なので、コントラストの基準は文字と同じ厳しさで見ます。
書体が主役なら、取得に失敗しない形で持つ
書体はランタイム取得をやめて、アプリに同梱しました。オフラインの初回起動でフォールバックすると、日本語アプリとして見た目が壊れます。書体が刷新の主役である以上、「取得できないことがある」形では持てません。
日本語の書体はサブセットできません。やること名やタブ名は自由入力で、任意の漢字が来ます。必要なウェイトをフルで同梱し、約 8MB の増加を受け入れました。
副産物として、スクリーンショット撮影のテストがテーマを迂回していたのを解消できました。ランタイム取得がテスト環境で例外を投げるため、配色だけ手で複製して本番のテーマを通していなかったのですが、同梱でその理由が消えました。結果、撮影される配色とダークテーマが本番と一致します。
テーマの適用点は1箇所に寄せ、冪等にする
こども向けのタイポグラフィは、最初はこどもホームとそこから開くシート類にしか効いていませんでした。図鑑・アルバム・スタンプ選択のように 画面を push で開くルートは Theme を継承しない ためです。
画面ごとに包む案は却下しました。画面が増えるたびに包み忘れます。採用したのは、ロールを見てアプリ全体に掛ける形です。こどもはルーティングの制御でおとな向け画面に到達しないので、「こどもロールならアプリ全体」で「こども画面すべて」が成立します。
// 適用点は1箇所。ロールが未確定のあいだは包まない
class ChildThemeForRole extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final role = ref.watch(currentUserProvider).asData?.value?.role;
if (role != Role.child) return child;
return ChildTheme(child: child); // 冪等なので入れ子でも結果は同じ
}
}
このテーマは 冪等 にしてあります。書体とサイズを上書きするだけで、相対的な加算をしません。そのため、こどもホームが自分でも包んだままにしておけます。画面を単体でマウントするケース(ウィジェットテスト、撮影ハーネス、おとな端末に置くこども用のタブ)でも、こどもの見た目が成立します。適用点を1つに絞るぶん、二重適用を許す作りにしておく という組み合わせです。
キャラクターを主役にする
こどもホームの上部は、白い四角に収まったキャラクターの隣に「17/20」の数字と3行の説明文が並ぶ構成でした。こどもには読み物です。分数は読み取りにくく、説明文は毎日読むものでもありません。
外したのは分数と説明文です。説明は「?」を押したときだけ出します。残したのは進捗のバーと節目の丸で、これは 次に何が起きるかを持つ情報 なので、アイコン数個には置き換えられません。数字のかわりに「あと 3こで さいごの すがた!」という次の節目基準の文にしました。
キャラクターの周りは空から草原へのグラデーションにして、背景をこの層だけに閉じています。将来「背景の着せ替え」を載せるとき、差し替えだけで済みます。
浮遊アニメーションは有限回(2往復)で止めました。無限ループにすると、自動テストの待ち合わせが永久に完了しません。常時 GPU を回す電力コストも払い続けます。開いた瞬間に生きていると伝われば目的は足りる ので、続ける必要がありません。動きを減らす設定が有効なときは動かしません。
まとめ
- 見た目の刷新は、語彙を1つ決めてから始める。決まっていれば、部品を足すときに迷わない
- 状態は形と位置で示し、色は利用者に開放する。結果としてコントラストや色覚特性の問題が外れる
- 地の色がブランドなら、コントラストは上に載る側で解く。検証は自動化できるが、どちらを動かすかは判断
- 書体が主役なら、取得に失敗しない形で持つ。サイズの増加はその対価として見る
- テーマの適用点は1つに寄せ、そのぶん冪等にして二重適用を許す
- 常時動くアニメーションは有限回で止める。テストの待ち合わせと電力の両方に効く