From ff20eee32fc28bb7b2403ef1bc08802c048f305e Mon Sep 17 00:00:00 2001 From: Badr Kouki Date: Sat, 9 Aug 2025 11:16:43 +0100 Subject: [PATCH 1/2] feat: implement hour restriction to date time picker --- example/lib/main.dart | 14 ++++++++ lib/bottom_picker.dart | 4 +++ lib/cupertino/cupertino_date_picker.dart | 46 ++++++++++++++++++++++-- lib/widgets/date_picker.dart | 3 ++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 82c655f..3220c19 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -100,6 +100,17 @@ class ExampleApp extends StatelessWidget { ), ), ), + SizedBox( + width: buttonWidth, + child: ElevatedButton( + onPressed: () { + _openDateTimePicker( + context, + ); + }, + child: Text('Date time Picker', textAlign: TextAlign.center), + ), + ), SizedBox( width: buttonWidth, child: ElevatedButton( @@ -599,6 +610,9 @@ class ExampleApp extends StatelessWidget { BuildContext context, ) { BottomPicker.dateTime( + hourPredicate: (hour) { + return hour > 6; + }, headerBuilder: (context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/bottom_picker.dart b/lib/bottom_picker.dart index 6f9ee5e..f4fb1e7 100644 --- a/lib/bottom_picker.dart +++ b/lib/bottom_picker.dart @@ -277,6 +277,7 @@ class BottomPicker extends StatefulWidget { this.headerBuilder, this.calendarDays = CupertinoDatePickerWidget.fullWeek, this.diameterRatio = 1.1, + this.hourPredicate, }) { datePickerMode = CupertinoDatePickerMode.dateAndTime; bottomPickerType = BottomPickerType.dateTime; @@ -811,6 +812,8 @@ class BottomPicker extends StatefulWidget { /// The bottom picker selector diameter ratio. final double diameterRatio; + SelectableHourPredicate? hourPredicate; + ///display the bottom picker popup ///[context] the app context to display the popup void show(BuildContext context) { @@ -1023,6 +1026,7 @@ class BottomPickerState extends State { itemExtent: widget.itemExtent, showTimeSeparator: widget.showTimeSeparator, pickerThemeData: widget.pickerThemeData, + hourPredicate: widget.hourPredicate, ) : widget.bottomPickerType == BottomPickerType.year diff --git a/lib/cupertino/cupertino_date_picker.dart b/lib/cupertino/cupertino_date_picker.dart index 2d12a9c..2ab5667 100644 --- a/lib/cupertino/cupertino_date_picker.dart +++ b/lib/cupertino/cupertino_date_picker.dart @@ -4,7 +4,6 @@ library; import 'dart:math' as math; - import 'package:flutter/cupertino.dart'; import 'package:flutter/scheduler.dart'; @@ -47,6 +46,12 @@ const double _kTimerPickerLabelFontSize = 17.0; // The width of each column of the countdown time picker. const double _kTimerPickerColumnIntrinsicWidth = 106; +/// Signature for predicating dates for enabled date selections. +/// +/// See [showDatePicker], which has a [SelectableDayPredicate] parameter used +/// to specify allowable days in the date picker. +typedef SelectableHourPredicate = bool Function(int hour); + TextStyle _themeTextStyle(BuildContext context, {bool isValid = true}) { final TextStyle style = CupertinoTheme.of(context).textTheme.dateTimePickerTextStyle; @@ -284,6 +289,7 @@ class CupertinoDatePickerWidget extends StatefulWidget { this.selectionOverlayBuilder, this.showTimeSeparator = false, this.calendarDays = fullWeek, + this.selectableHourPredicate, }) : initialDateTime = initialDateTime ?? DateTime.now(), assert( itemExtent > 0, @@ -336,6 +342,11 @@ class CupertinoDatePickerWidget extends StatefulWidget { this.initialDateTime.minute % minuteInterval == 0, 'initial minute is not divisible by minute interval', ); + assert( + (selectableHourPredicate == null) || + (selectableHourPredicate?.call(this.initialDateTime.hour) == true), + 'Hour must satisfy the hour predicate', + ); } /// The mode of the date picker as one of [CupertinoDatePickerMode]. Defaults @@ -468,6 +479,9 @@ class CupertinoDatePickerWidget extends StatefulWidget { /// {@end-tool} final SelectionOverlayBuilder? selectionOverlayBuilder; + //TODO add docs + final SelectableHourPredicate? selectableHourPredicate; + static const List fullWeek = [ DateTime.monday, DateTime.tuesday, @@ -853,6 +867,8 @@ class _CupertinoDatePickerDateTimeState if (isDateInvalid) { return; + } else if (widget.selectableHourPredicate?.call(selected.hour) == false) { + return; } widget.onDateTimeChanged(selected); @@ -939,18 +955,23 @@ class _CupertinoDatePickerDateTimeState // `hourIndex`, is it possible to change the value of the minute picker, so // that the resulting date stays in the valid range. bool _isValidHour(int meridiemIndex, int hourIndex) { + int selectedHour = _selectedHour(meridiemIndex, hourIndex); final DateTime rangeStart = DateTime( initialDateTime.year, initialDateTime.month, initialDateTime.day + selectedDayFromInitial, - _selectedHour(meridiemIndex, hourIndex), + selectedHour, ); // The end value of the range is exclusive, i.e. [rangeStart, rangeEnd). final DateTime rangeEnd = rangeStart.add(const Duration(hours: 1)); + bool hourInIntevalPredicate = + widget.selectableHourPredicate?.call(selectedHour) ?? true; + return (widget.minimumDate?.isBefore(rangeEnd) ?? true) && - !(widget.maximumDate?.isBefore(rangeStart) ?? false); + !(widget.maximumDate?.isBefore(rangeStart) ?? false) && + hourInIntevalPredicate; } Widget _buildHourPicker( @@ -1176,6 +1197,24 @@ class _CupertinoDatePickerDateTimeState } } + void _checkOnHourDisplay() { + final DateTime selectedDate = selectedDateTime; + final bool minCheck = widget.minimumDate?.isAfter(selectedDate) ?? false; + + if (widget.selectableHourPredicate?.call(selectedDate.hour) == false) { + const int daysThreshold = 1; + final DateTime targetDate = + selectedDate.add(const Duration(hours: daysThreshold)); + + _scrollToDate( + targetDate, + selectedDate, + minCheck, + newItemIndex: dateController.selectedItem + daysThreshold, + ); + } + } + // One or more pickers have just stopped scrolling. void _pickerDidStopScrolling() { // Call setState to update the greyed out date/hour/minute/meridiem. @@ -1193,6 +1232,7 @@ class _CupertinoDatePickerDateTimeState final bool maxCheck = widget.maximumDate?.isBefore(selectedDate) ?? false; _checkOnCustomDaysDisplay(); + _checkOnHourDisplay(); if (minCheck || maxCheck) { // We have minCheck === !maxCheck. final DateTime targetDate = diff --git a/lib/widgets/date_picker.dart b/lib/widgets/date_picker.dart index a58014f..41483ae 100644 --- a/lib/widgets/date_picker.dart +++ b/lib/widgets/date_picker.dart @@ -15,6 +15,7 @@ class DatePicker extends StatelessWidget { final bool showTimeSeparator; final List calendarDays; final CupertinoTextThemeData? pickerThemeData; + final SelectableHourPredicate? hourPredicate; const DatePicker({ super.key, @@ -31,6 +32,7 @@ class DatePicker extends StatelessWidget { this.showTimeSeparator = false, this.calendarDays = CupertinoDatePickerWidget.fullWeek, this.pickerThemeData, + this.hourPredicate, }); @override @@ -46,6 +48,7 @@ class DatePicker extends StatelessWidget { itemExtent: itemExtent ?? 0, showTimeSeparator: showTimeSeparator, mode: mode, + selectableHourPredicate: hourPredicate, onDateTimeChanged: onDateChanged, initialDateTime: initialDateTime, minuteInterval: minuteInterval, From bb5542e34fb147b16fc2a462794d8c84af16970e Mon Sep 17 00:00:00 2001 From: Badr Kouki Date: Sat, 9 Aug 2025 17:18:05 +0100 Subject: [PATCH 2/2] feat: update code documentation --- lib/bottom_picker.dart | 1 + lib/cupertino/cupertino_date_picker.dart | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/bottom_picker.dart b/lib/bottom_picker.dart index f4fb1e7..e6c658d 100644 --- a/lib/bottom_picker.dart +++ b/lib/bottom_picker.dart @@ -812,6 +812,7 @@ class BottomPicker extends StatefulWidget { /// The bottom picker selector diameter ratio. final double diameterRatio; + /// A predicate that can be used to select which hours are selectable. SelectableHourPredicate? hourPredicate; ///display the bottom picker popup diff --git a/lib/cupertino/cupertino_date_picker.dart b/lib/cupertino/cupertino_date_picker.dart index 2ab5667..0950d79 100644 --- a/lib/cupertino/cupertino_date_picker.dart +++ b/lib/cupertino/cupertino_date_picker.dart @@ -46,10 +46,7 @@ const double _kTimerPickerLabelFontSize = 17.0; // The width of each column of the countdown time picker. const double _kTimerPickerColumnIntrinsicWidth = 106; -/// Signature for predicating dates for enabled date selections. -/// -/// See [showDatePicker], which has a [SelectableDayPredicate] parameter used -/// to specify allowable days in the date picker. +/// Signature for predicating hour for enabled hours selections. typedef SelectableHourPredicate = bool Function(int hour); TextStyle _themeTextStyle(BuildContext context, {bool isValid = true}) {