6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Flutter]CupertinoDatePickerの日時を操作する

Last updated at Posted at 2020-09-06

CupertinoDatePickerの日時を操作する手段は、ない

こちらのissueにも上がっているように、CupertinoDatePickerの値(日時)を外から操作して設定する手段は提供されていません。
https://github.com/flutter/flutter/issues/42869

よくあるUIだと、乗換検索アプリの出発時刻設定画面で、「現在時刻に設定する」などがあると思います。
無理矢理対応するやり方だとすると、以下のようにwidgetの再構築を促す方法があります。設定する値は、initialDateで設定します。

CupertinoDatePicker(
  // keyを指定することでwidgetが再利用されないようにして、再構築されるようにする
  key: UniqueKey(),
  // 更新したい値
  initialDateTime: date,
  ...
)

CupertinoDatePickerをベースにTimePickerを作る

完成したものはこちらです。

image.png

CupertinoDatePickerをベースに、必要最低限の機能を切り出しています。
今回は単純にするため、AM/PMの表示設定や、日付の設定、l10nなどは全て排除しています。
また、個人的な好みで時と分の間にコロン【:】を配置しています。

外から時刻を設定するための手段は、後述のTimePickerController経由で操作できるようにしています。

実装方法

Widget本体

中で出てくるマジックナンバーは、ほとんどが大元のCupertinoDatePickerから拝借してきたものです。必要に応じて変更してください。

time_picker.dart
// 時・分の表示カラムの幅です
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単体で、CupertinoDatePickerCupertinoPickerを複数組み合わせたものになっています。なので、今回はCupertinoPickerを二つだけ持てば良いため、かなりシンプルになります。

CupertinoDatePickerのコードを見るととても複雑に見えてしまいますが、それはAM/PMの表示切替や日付の表示有無など、条件分岐が多くなってしまっているためです。なので、用途・機能を限定すれば、レイアウトの組み方自体は難しくありません。

また、CupertinoPickerの操作自体は、実はFixedExtentScrollControllerで元々可能になっています。
CupertinoDatePickerで操作できないのは、このFixedExtentScrollControllerが内部で自動生成され、隠蔽化されているためです。
なので、この自作Widgetの本質は【FixedExtentScrollControllerを外からどう操作できるようにするのか】という点となります。

操作用Controkker

上述のFixedExtentScrollControllerはここでも内部で自動生成し、意識はさせないようにしていますが、このFixedExtentScrollControllerへのアクセサとしてsetTimeを用意しました。これで自由に時刻を操作することが可能になります。

time_picker.dart
// [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}');
  }
}
6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?