より良いユーザー体験を求めて "ボタン" を深掘りする
いきなりですが、個人開発の良さとはなんでしょうか?
私は「技術的チャレンジをしやすい」という点に魅力を感じています。
業務上の開発ではアウトプットを最優先にする必要があるため、コスパ良く実現することを求められます。
しかし、個人開発に締切ありませんし、開発の優先順位も自分で決められます。
この記事に書いてあること
- Hover,Pressedを考慮したボタン
- アクセシビリティを考慮したボタン
- デスクトップアプリを考慮したボタン
そんなボタンを作る方法を考えていきます。
また、この記事で書いているようなことを個人で作っているメモアプリで実践しています。
よかったらインストールして触ってみてください。
Buttonをイチから作る。
Flutterで提供されているButton系のWidgetを使えば、簡単にボタンを実装できますがどうしてもマテリアルデザインよりの見た目になってしまいます。
もちろんカスタマイズもできるのですが、個人開発ということでイチからButtonを作ってみようと思います。
イチからButtonを作るときに考慮することはなんでしょうか?
GestureDetectorを使うだけではボタンとはいえません。
ボタンにどんな機能が存在しているのかを探っていこうと思います。
そのためにまず、お馴染みのElevatedButtonを見ていきます。
Buttonとは...ElevatedButtonにダイブする。
みなさんよく使うElevatedButtonの中身を辿っていくといくつかのWidgetで構成されています。
今回議論したいWidgetだけを抜粋しています。
- Semantics
- InkWell
- Actions
- Focus
- MouseRegion
- GestureDetector
これらからわかることは、ボタンを作る際には以下のことを考慮する必要がありそうです。
- Hover、PressedなどUIの状態の考慮
- デスクトップアプリの考慮
- アクセシビリティの考慮
個人的に面白いと思った順番に並べています。上から見ていきましょう。
Hover、PressedなどUIの状態を考慮する
InkWellWidgetは押している間にリップルエフェクトを表現してくれるWidgetです。
実装で使ったことがある人は多いのではないでしょうか。
実はInkWellは押している間だけ変化するのではなく、ホバーしている間なども背景カラーが変わります。

参考 https://api.flutter.dev/flutter/material/InkWell-class.html
ホバーのようなユーザーのインタラクションによって変化するもは他に、こんなものがあります。
-
Enabled: ボタンが押せることを表す状態 -
Disabled: ボタンが押せないことを表す状態 -
Hover: マウスでホバーしている状態 -
Focused: フォーカスしている状態 -
Pressed: 押している状態 -
Dragged: ドラッグしている状態(今回は考慮しない)
身の回りにあるボタンを触ってみるとわかりますが、多くのボタンはそれぞれの状態で色や形が変わったりします。
どう設計、実装すれば良いのかをマテリアルデザインを参考に考えてみます。

Zennの保存ボタン。ショートカットを教えてくれる。
Hover、Disabledのカラーは用意しない!
マテリアルデザインではHoverやDisabledのカラーを表現するためにそれ専用のカラーを用意はせず、State layersという概念を使っています。
これは、背景色の上に半透明のレイヤーを重ねることでユーザーが見た時のカラーを変えています。
半透明のレイヤーはアイコンやテキストのカラーを元に、Hoverであれば8%の透明度、Pressであれば10%...と定義されています。
(Flutterで表現するのであればwithOpacityを使います。)

https://m3.material.io/foundations/interaction/states/state-layers
State layerの利点
State layerを取り入れれることの利点は、毎回Hover color、Pressed colorなどを定義しなくても良いことです。
背景カラーとコンテンツ(テキストやアイコン)のカラーが決まれば自ずとHoverカラーなども決まってくるため、システマチックです。
これは完全なイメージですが、Flutterで表すとこんな感じでしょうか。
const containerColor = Colors.xxx;
Stack(
children: [
Background(),
StateLayer(color: containerColor.withOpacity(0.08)),
Text('text', style: TextStyle(color: containerColor)),
],
)
個人的にはリップルエフェクトより...
これは完全好みの話なのですが、マテリアルデザインはPressed状態をリップルエフェクトで表現していますが、自分はiOS利用歴が長く、あまり好みではありません...
個人的に好きなPressed状態は、ボタン自体が少し小さくなり、バウンスするようなものが好きだったりします。

Arcのタブを押した時
デスクトップアプリの考慮
次に考慮していくのはMouseRegion、Focus,Actionのことです。
これらはモバイルアプリの開発だとあまり意識して使うことは少ないと思いますが、デスクトップアプリを作る場合は必須です。
MouseRegionでカーソルを意識する
デスクトップアプリは、ボタンにホバーするとクリック可能であることを記す形状に変わります。
先ほど見たInkWellのGIFでも変化しています。
Disabled(押せない時)には変化せず、押せる時だけ変化するような考慮も必要でしょう。

参考 https://api.flutter.dev/flutter/material/InkWell-class.html
Focusを意識する
モバイルアプリでよく使うTextFieldWidgetの内部で使われているFocusWidgetです。
Focusはキーボードのフォーカスを取得するもので、TextFieldだけではなくボタンにもフォーカスします。
たとえば、Slackのメッセージ入力箇所ではTabキーや矢印キーでフォーカスをボタン間で移動できます。

Actionsでショートカットを意識する
ボタンにフォーカスがある場合に、Enterキーを押すことでタップした時と同じ処理を行えるのが一般的です。
そのためにActionWidgetがあります。
実装の詳細は割愛しますが、キーとそのキーの押された時の処理を渡すことでいろんなことができそうです。
FocusableActionDetectorで楽しちゃう
実はMouseRegion、Focus,ActionをまとめたようなFocusableActionDetectorというWidgetがあります。
これを使えばサクッとデスクトップアプリに対応できちゃうということですね!!
FocusableActionDetector(
mouseCursor: SystemMouseCursors.click,
actions: <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: (_) => onInvoke()),
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
onInvoke: (_) => onInvoke(),
),
},
),
実際に内部的にもMouseRegion、Focus、Actionを使っています。
Semanticはアクセシビリティ
SemanticはWidgetに意味を持たせるためのWidgetです。
一番有名なのはiOSのVoiceOver機能を使ってスクリーンリーダーを行う際に使います。
ButtonStyleButtonのSemantic自体にはlabel(スクリーンリーダーで読み上げるテキスト)はないのですが、それはText Widgetにあります。
Buttonを再発明してみよう!
こうみるとButtonも意外と奥が深いことがわかります。
もちろん、ElevatedButtonはもっと複雑な処理が書かれていますがその一部を理解できたのでGestureDetectorを使うよりはボランらしいボタンを実装できるのではないでしょうか。
みなさんもぜひ個人開発等でチャレンジしてみてください!!
Flutterのことだけではなく、デザインやFigmaのことも発信したりしています。
一応Twitterを載せておくのでよかったらフォローお願いします!
この記事にいいねもぜひ!
また、この記事で書いているようなことを個人で作っているメモアプリで実践しています。
よかったらインストールして触ってみてください。
もっとボタンに詳しくなりたい方への参考記事たち
Discussion