👆

より良いユーザー体験を求めて "ボタン" を深掘りする

2024/07/11に公開

いきなりですが、個人開発の良さとはなんでしょうか?

私は「技術的チャレンジをしやすい」という点に魅力を感じています。

業務上の開発ではアウトプットを最優先にする必要があるため、コスパ良く実現することを求められます。
しかし、個人開発に締切ありませんし、開発の優先順位も自分で決められます。

この記事に書いてあること

  • Hover,Pressedを考慮したボタン
  • アクセシビリティを考慮したボタン
  • デスクトップアプリを考慮したボタン

そんなボタンを作る方法を考えていきます。

また、この記事で書いているようなことを個人で作っているメモアプリで実践しています。
よかったらインストールして触ってみてください。
https://apps.apple.com/jp/app/tokeru/id6479510993

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のタブを押した時

デスクトップアプリの考慮

次に考慮していくのはMouseRegionFocus,Actionのことです。
これらはモバイルアプリの開発だとあまり意識して使うことは少ないと思いますが、デスクトップアプリを作る場合は必須です。

MouseRegionでカーソルを意識する

デスクトップアプリは、ボタンにホバーするとクリック可能であることを記す形状に変わります。
先ほど見たInkWellのGIFでも変化しています。
Disabled(押せない時)には変化せず、押せる時だけ変化するような考慮も必要でしょう。


参考 https://api.flutter.dev/flutter/material/InkWell-class.html

Focusを意識する

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

Actionsでショートカットを意識する

ボタンにフォーカスがある場合に、Enterキーを押すことでタップした時と同じ処理を行えるのが一般的です。
そのためにActionWidgetがあります。
実装の詳細は割愛しますが、キーとそのキーの押された時の処理を渡すことでいろんなことができそうです。

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

FocusableActionDetectorで楽しちゃう

実はMouseRegionFocus,ActionをまとめたようなFocusableActionDetectorというWidgetがあります。
これを使えばサクッとデスクトップアプリに対応できちゃうということですね!!

FocusableActionDetector(
  mouseCursor: SystemMouseCursors.click,
  actions: <Type, Action<Intent>>{
    ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: (_) => onInvoke()),
    ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
      onInvoke: (_) => onInvoke(),
    ),
  },
),

実際に内部的にもMouseRegionFocusActionを使っています。

https://github.com/flutter/flutter/blob/9e04246c482796e74eac5337f8ba926bcc878f64/packages/flutter/lib/src/widgets/actions.dart#L1364-L1383

Semanticはアクセシビリティ

SemanticはWidgetに意味を持たせるためのWidgetです。
一番有名なのはiOSのVoiceOver機能を使ってスクリーンリーダーを行う際に使います。
ButtonStyleButtonのSemantic自体にはlabel(スクリーンリーダーで読み上げるテキスト)はないのですが、それはText Widgetにあります。

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

Buttonを再発明してみよう!

こうみるとButtonも意外と奥が深いことがわかります。
もちろん、ElevatedButtonはもっと複雑な処理が書かれていますがその一部を理解できたのでGestureDetectorを使うよりはボランらしいボタンを実装できるのではないでしょうか。
みなさんもぜひ個人開発等でチャレンジしてみてください!!

Flutterのことだけではなく、デザインやFigmaのことも発信したりしています。
一応Twitterを載せておくのでよかったらフォローお願いします!
https://x.com/imasirooo

この記事にいいねもぜひ!

また、この記事で書いているようなことを個人で作っているメモアプリで実践しています。
よかったらインストールして触ってみてください。
https://apps.apple.com/jp/app/tokeru/id6479510993

もっとボタンに詳しくなりたい方への参考記事たち

https://m3.material.io/foundations/interaction/states/state-layers

https://goodpatch-tech.hatenablog.com/entry/state-layer-for-interaction-states

Discussion