ユーザーに何か情報を入力してもらい、保存ボタンを押したときにHUDを表示するのではなく、画面のインタラクションを制御しつつ、保存ボタンに進捗を表示して表現したいなと思いProgressButtonなるものを自作してみました。
仕様
汎用的、というよりは自分が今開発しているアプリで必要な機能に絞りつつ、以下のような仕様で実装することにしました。
そこまで難しい状態管理やロジックもありません。
- StatelessWidgetで作成する
- ボタンの状態はButton内で管理はせず、親WidgetからProvider等を活用して受け渡ししてもらう
- ユーザーが情報を入力し、それが有効なデータだった場合にボタンを有効化したいため、
enableの切り替えができるようにする - ユーザーがボタンを押し、何かしらの処理が実行開始したら
processingをtrueにし、ボタンにインジケーターを表示する - 処理が完了したら
processingをfalseにし、ボタンを元の状態に戻せるようにする enable=falseまたはprocessing=trueのときはボタンが押せないようにするprocessing=trueの時はボタンの背景色を少し暗めにする- ボタンのタイトルは常に中央揃えにし、インジケーターを表示しているとき/していない時でラベルの位置がずれないように工夫する
コード
丁寧に変数、定数定義したりWidgetの分割をしたため100行弱になりましたが、1つのbuild関数に収めると50行ほどのコード量で収まると思います。
詳細説明
RaisedButton Widgetをベースに作成しています。ボタンの両端を丸くするためにStadiumBorder をshapeに指定しています。
ボタンのサイズは高さは固定値、幅はdouble.infinityを指定して親要素に合わせて拡がれる限り拡がるようにしています。
処理実行中をCircularProgressIndicator Widetを使って表し、実行中でないときは、それを包んでいるVisibility Widgetのvisibleの切り替えによって非表示にしています。
※実際の開発で使っているProgressButtonの色合いはlight/darkモードによって色が変わるような処理を入れているため、例ではColors.blueなどの色を代わりに使用しています。
完成品

また、DartPadにもデモをあげてみたのでそちらでも挙動が確認できます。 デモではボタンを押すとボタンが切り替わり、2秒後に元に戻るようになっています。
工夫した点など
SizedBoxの活用
CircularProgressIndicatorを任意のサイズにするためにSizedBoxを使っています。他にも要素間のスペース調整や、ボタン自体のサイズを決める際にもSizedBoxを使っています。便利。
要素のAlignment
また、インジケーターが表示されても非表示になってもラベルは常に中央にするために、Row Widgetを使い、
- インジケーター(A)
- 要素間のスペース(B)
- ラベル
- (AとBの横幅を足したサイズのSizedBox)
とchildrenを挿入し、axisをcenterにすることで実現しています。
disable時のタップ抑制と色の切り替え
また、processing || !enabled な時はonPressにnullを渡してボタンの状態をdisabledに変更するようにしてタッチを無効化しつつ、ボタンの色がdisabledColorに切り替わるようにしました。
disabledColorは、processing時は通常時のbackgroundColorを半透明にしたもの、そうでなければグレーの色合いを指定しています。
参考にしたもの
今回お見せしたProgressButton、少し前まではprocessing中は文字を消してインジケーターのみを表示していたのですが、以下の記事を見てそれはあまりよろしくないということで「インジケーター+ラベル」の形かつ、ラベルは常に中央揃えで表示できるように改修しました。