CupertinoDatePickerの日時を操作する手段は、ない
こちらのissueにも上がっているように、CupertinoDatePickerの値(日時)を外から操作して設定する手段は提供されていません。
https://github.com/flutter/flutter/issues/42869
よくあるUIだと、乗換検索アプリの出発時刻設定画面で、「現在時刻に設定する」などがあると思います。
無理矢理対応するやり方だとすると、以下のようにwidgetの再構築を促す方法があります。設定する値は、initialDate
で設定します。
CupertinoDatePicker(
// keyを指定することでwidgetが再利用されないようにして、再構築されるようにする
key: UniqueKey(),
// 更新したい値
initialDateTime: date,
...
)
CupertinoDatePicker
をベースにTimePickerを作る
完成したものはこちらです。
CupertinoDatePickerをベースに、必要最低限の機能を切り出しています。
今回は単純にするため、AM/PMの表示設定や、日付の設定、l10nなどは全て排除しています。
また、個人的な好みで時と分の間にコロン【:】を配置しています。
外から時刻を設定するための手段は、後述のTimePickerController
経由で操作できるようにしています。
実装方法
Widget本体
中で出てくるマジックナンバーは、ほとんどが大元のCupertinoDatePickerから拝借してきたものです。必要に応じて変更してください。
// 時・分の表示カラムの幅です
const double _kColumnWidths = 48.0;
class TimePicker extends StatefulWidget {
// 後述の[TimePickerController]が必須パラメータです。[TimePickerController]経由で日時の操作・取得を行います。
const TimePicker({Key key, @required this.controller}) : super(key: key);
final TimePickerController controller;
@override
_TimePickerState createState() => _TimePickerState();
}
class _TimePickerState extends State<TimePicker> {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildHourPicker(),
// 時、分の間のコロンです。私の好みで追加しました。
_buildSeparator(),
_buildMinutePicker(),
],
);
}
/// 「時」選択Picker
Widget _buildHourPicker() {
return _buildPicker(24, widget.controller._hourController);
}
/// 「分」選択Picker
Widget _buildMinutePicker() {
return _buildPicker(60, widget.controller._minuteController);
}
/// Picker生成
/// [size]の分のPickerを作ります。操作できるように[FixedExtentScrollController]を渡してください
Widget _buildPicker(int size, FixedExtentScrollController controller) {
return SizedBox(
width: _kColumnWidths,
// 数値は[CupertinoDatePicker]を参考にしています
child: CupertinoPicker(
backgroundColor: Colors.white,
scrollController: controller,
offAxisFraction: 0.45,
itemExtent: 32,
useMagnifier: true,
magnification: 2.35 / 2.1,
squeeze: 1.25,
looping: true,
onSelectedItemChanged: (_) {},
children: List.generate(size, (int index) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SizedBox(
width: _kColumnWidths, child: _buildLabel(context, index)),
),
);
}),
),
);
}
/// Pickerの選択肢となるテキストラベルを生成します
Widget _buildLabel(BuildContext context, int number) {
return Text(
number.toString().padLeft(2, '0'),
style: CupertinoTheme.of(context).textTheme.dateTimePickerTextStyle,
);
}
Widget _buildSeparator() {
return const Text(' : ',
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.w500));
}
@override
void dispose() {
widget.controller._dispose();
super.dispose();
}
}
CupertinoPickerがPicker単体で、CupertinoDatePicker
はCupertinoPicker
を複数組み合わせたものになっています。なので、今回はCupertinoPicker
を二つだけ持てば良いため、かなりシンプルになります。
CupertinoDatePicker
のコードを見るととても複雑に見えてしまいますが、それはAM/PMの表示切替や日付の表示有無など、条件分岐が多くなってしまっているためです。なので、用途・機能を限定すれば、レイアウトの組み方自体は難しくありません。
また、CupertinoPicker
の操作自体は、実はFixedExtentScrollControllerで元々可能になっています。
CupertinoDatePicker
で操作できないのは、このFixedExtentScrollController
が内部で自動生成され、隠蔽化されているためです。
なので、この自作Widgetの本質は【FixedExtentScrollController
を外からどう操作できるようにするのか】という点となります。
操作用Controkker
上述のFixedExtentScrollController
はここでも内部で自動生成し、意識はさせないようにしていますが、このFixedExtentScrollController
へのアクセサとしてsetTime
を用意しました。これで自由に時刻を操作することが可能になります。
// [TimePicker]と同じファイルに記述します
class TimePickerController {
TimePickerController()
: _hourController = FixedExtentScrollController(initialItem: 0),
_minuteController = FixedExtentScrollController(initialItem: 0);
final FixedExtentScrollController _hourController;
final FixedExtentScrollController _minuteController;
/// 時刻の設定。これが今回一番実現したかったこと。
void setTime(DateTime time) {
_hourController.animateToItem(time.hour,
duration: const Duration(milliseconds: 100), curve: Curves.easeIn);
_minuteController.animateToItem(time.minute,
duration: const Duration(milliseconds: 100), curve: Curves.easeIn);
}
int get hour {
if (!_hourController.hasClients) {
return 0;
}
// 何周もすると、マイナスにも24以上の値にもなってしまうため、あまりを求める
return _hourController.selectedItem % 24;
}
int get minute {
if (!_minuteController.hasClients) {
return 0;
}
return _minuteController.selectedItem % 60;
}
/// [TimePicker]内で自動で開放します
void _dispose() {
_hourController.dispose();
_minuteController.dispose();
}
}
初期値は00:00
にしてしまいましたが、コンストラクタで指定できた方が便利かもしれません。
使い方
class Sample extends StatelessWidget {
// 操作用に[TimePickerController]を保持します。引数は不要です。disposeも自動で行うため、不要です。
final _controller = TimePickerController();
@override
Widget build(BuildContext context) {
return Column(
children: [
TimePicker(controller: _controller),
// 現在時刻に戻すボタン
IconButton(
icon: const Icon(Icons.access_time),
onPressed: resetTime,
)
],
);
}
void resetTime() {
// [TimePickerController]に対して`DateTime`を設定するだけです。
_controller.setTime(DateTime.now());
}
void printTime() {
// 値の取得も[TimePickerController]経由で`hour`/`minute`でアクセスできます。
print('time = ${_controller.hour} : ${_controller.minute}');
}
}