⏰
flutter_datetime_picker で5分単位などカスタム選択肢を表示する
はじめに
flutter_datetime_picker は iOS でおなじみの日時選択のピッカーを Flutter で使えるパッケージです。
dart 3.0/Flutter 3.10 以降に対応した flutter_datetime_picker_plus もフォークされて提供されています。
このパッケージでは分の選択がデフォルトでは1分単位選択できますが、5分や15分単位での選択肢を表示させるオプションが存在していませんでした。
1分単位ではアプリのユースケース上細かすぎたので、選択肢をカスタマイズしてみました。
つまりこういうこと
やったこと
- 何分単位にしたいかを定義して、外から渡す
- カスタマイズ方法を参考にカスタマイズ
- カスタマイズしたモデルを使って、DatePicker を呼び出す
何分単位にしたいかの定義
アプリや機能によって、5分単位, 10分単位など何分間隔で分の選択肢を表示したいかは、まちまちでしょう。
今回は↓のようなenum を定義して、DatePicker 表示時に渡すようにしました。
/// DatePicker の分の選択肢をどの間隔にするか
enum MinuteInterval {
five(5),
ten(10),
fifteen(15),
thirty(30);
const MinuteInterval(this.minute);
final int minute;
}
※20分の選択肢は筆者のユースケースで不要だったので除いていますが、追加しても問題ありません。
※60で割り切れない選択肢を増やした場合の挙動は未確認です
※int で渡すと、負数や 60 以上の数を渡される可能性があるため、enum で取り得るパラメータの選択肢を限定しています。
DatePicker のカスタマイズ
CommonPickerModel
派生のモデルを定義して、 DatePicker
に渡してあげれば OK です。
前提として、
- Picker には3列表示できる
- それぞれ、left, middle, right と内部的には管理されている
(利用シーンによって、年・月・日だったり、時・分・秒だったりするのでこの名前なんだろう) - 選択中の DateTime オブジェクトを finalTime 関数で返す
あたりを押さえておけば、実装の理解が捗るかと思います。
実際のコード
class CustomPicker extends CommonPickerModel {
CustomPicker({
required DateTime? currentTime,
required LocaleType locale,
required this.minuteInterval,
}) : super(locale: locale) {
this.currentTime = currentTime ?? DateTime.now();
// 時は0〜23なのでそのまま index と対応している
setLeftIndex(this.currentTime.hour);
// interval を考慮して index を指定
// 5分の interval の場合、要素は[0,5,10,...55]
// currentTime.minute が 10 なら index は 2 になる
setMiddleIndex(this.currentTime.minute ~/ minuteInterval.minute);
// 秒は使わないので0を渡しておく(指定しないと LateInitializationError になる)
setRightIndex(0);
}
final MinuteInterval minuteInterval;
// value を length で指定された桁数で表示する。
// 桁数が足りないときは 0 埋めする
// ※ライブラリ内で定義されている関数を移植した
String digits(int value, int length) => '$value'.padLeft(length, '0');
// 時の表示文字列を要素の index から生成
// null を返せば選択肢として表示されない
String? leftStringAtIndex(int index) {
if (index >= 0 && index < 24) {
// 0 〜 24 は 2 桁で表示
return digits(index, 2);
} else {
// それ以外の要素は表示させない
return null;
}
}
String? middleStringAtIndex(int index) {
// index の上限は 60 ÷ minuteInterval.minute
// (60 / minuteInterval.minute).toInt() にすると division_optimization になるので
// 60 ~/ minuteInterval.minute としている
if (index >= 0 && index < 60 ~/ minuteInterval.minute) {
// 表示する分の文字列は index * interval
return digits(index * minuteInterval.minute, 2);
} else {
return null;
}
}
// 時と分の間の Divider 文字列
String leftDivider() => ':';
// 分と秒の間の Divider 文字列
// 秒を表示しないので空文字を返しておく
String rightDivider() => '';
// 時、分、秒のそれぞれの列の幅の比率
// 秒は使わないので 0 を指定
List<int> layoutProportions() => [1, 1, 0];
DateTime finalTime() {
return currentTime.isUtc
? DateTime.utc(
currentTime.year,
currentTime.month,
currentTime.day,
currentLeftIndex(),
// middle の index から分を算出
currentMiddleIndex() * minuteInterval.minute,
currentRightIndex(),
)
: DateTime(
currentTime.year,
currentTime.month,
currentTime.day,
currentLeftIndex(),
// middle の index から分を算出
currentMiddleIndex() * minuteInterval.minute,
currentRightIndex(),
);
}
}
呼び出し
DatePicker.showPicker(
context,
locale: LocaleType.jp,
onConfirm: (date) { ... },
pickerModel: CustomPicker(
currentTime: DateTime.now(),
minuteInterval: MinuteInterval.five,
locale: LocaleType.jp,
),
);
※showPicker にも pickerModel にも locale を指定するのが気になるところ…
(実装を追えばどちらが優先されるかはわかるかと思いますが、ここでは割愛🙇)
実行結果
意図して通りに動いてました 💯
five | ten | fifteen | thirty |
---|---|---|---|
補足
- 頑張れば繰り上がりの実装もできるかな…と思いつつも、iOS の picker がやってなかったので割愛
- 12h 表記にしたい場合などは、よしなにカスタマイズして下さい
Discussion