1
0
mirror of https://github.com/nisrulz/flutter-examples.git synced 2025-11-08 20:50:04 +00:00

New Example - Calendar (#91)

This commit is contained in:
Ishaan Kesarwani
2022-10-22 23:59:43 +05:30
committed by GitHub
parent d9cc97a7ea
commit 91d4d1b868
100 changed files with 7300 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
export 'package:flutter_date_pickers/src/date_period.dart';
export 'package:flutter_date_pickers/src/date_picker_keys.dart';
export 'package:flutter_date_pickers/src/layout_settings.dart';
export 'package:flutter_date_pickers/src/date_picker_styles.dart';
export 'package:flutter_date_pickers/src/event_decoration.dart';
export 'package:flutter_date_pickers/src/day_picker.dart';
export 'package:flutter_date_pickers/src/week_picker.dart';
export 'package:flutter_date_pickers/src/month_picker.dart';
export 'package:flutter_date_pickers/src/range_picker.dart';
export 'package:flutter_date_pickers/src/unselectable_period_error.dart';

View File

@@ -0,0 +1,347 @@
import 'package:flutter/material.dart';
import 'date_picker_mixin.dart';
import 'date_picker_styles.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
import 'utils.dart';
/// Widget for date pickers based on days and cover entire month.
/// Each cell of this picker is day.
class DayBasedPicker<T> extends StatelessWidget with CommonDatePickerFunctions {
/// Selection logic.
final ISelectablePicker selectablePicker;
/// The current date at the time the picker is displayed.
final DateTime currentDate;
/// The earliest date the user is permitted to pick.
/// (only year, month and day matter, time doesn't matter)
final DateTime firstDate;
/// The latest date the user is permitted to pick.
/// (only year, month and day matter, time doesn't matter)
final DateTime lastDate;
/// The month whose days are displayed by this picker.
final DateTime displayedMonth;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Key fo selected month (useful for integration tests)
final Key? selectedPeriodKey;
/// Styles what can be customized by user
final DatePickerRangeStyles datePickerStyles;
/// Builder to get event decoration for each date.
///
/// All event styles are overridden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
/// Localizations used to get strings for prev/next button tooltips,
/// weekday headers and display values for days numbers.
final MaterialLocalizations localizations;
/// Creates main date picker view where every cell is day.
DayBasedPicker(
{Key? key,
required this.currentDate,
required this.firstDate,
required this.lastDate,
required this.displayedMonth,
required this.datePickerLayoutSettings,
required this.datePickerStyles,
required this.selectablePicker,
required this.localizations,
this.selectedPeriodKey,
this.eventDecorationBuilder})
: assert(!firstDate.isAfter(lastDate)),
super(key: key);
@override
Widget build(BuildContext context) {
final List<Widget> labels = <Widget>[];
List<Widget> headers = _buildHeaders(localizations);
List<Widget> daysBeforeMonthStart = _buildCellsBeforeStart(localizations);
List<Widget> monthDays = _buildMonthCells(localizations);
List<Widget> daysAfterMonthEnd = _buildCellsAfterEnd(localizations);
labels.addAll(headers);
labels.addAll(daysBeforeMonthStart);
labels.addAll(monthDays);
labels.addAll(daysAfterMonthEnd);
return Padding(
padding: datePickerLayoutSettings.contentPadding,
child: Column(
children: <Widget>[
Flexible(
child: GridView.custom(
physics: datePickerLayoutSettings.scrollPhysics,
gridDelegate: datePickerLayoutSettings.dayPickerGridDelegate,
childrenDelegate:
SliverChildListDelegate(labels, addRepaintBoundaries: false),
),
),
],
),
);
}
List<Widget> _buildHeaders(MaterialLocalizations localizations) {
final int firstDayOfWeekIndex = datePickerStyles.firstDayOfeWeekIndex ??
localizations.firstDayOfWeekIndex;
DayHeaderStyleBuilder dayHeaderStyleBuilder =
datePickerStyles.dayHeaderStyleBuilder ??
// ignore: avoid_types_on_closure_parameters
(int i) => datePickerStyles.dayHeaderStyle;
List<Widget> headers = getDayHeaders(dayHeaderStyleBuilder,
localizations.narrowWeekdays, firstDayOfWeekIndex);
return headers;
}
List<Widget> _buildCellsBeforeStart(MaterialLocalizations localizations) {
List<Widget> result = [];
final int year = displayedMonth.year;
final int month = displayedMonth.month;
final int firstDayOfWeekIndex = datePickerStyles.firstDayOfeWeekIndex ??
localizations.firstDayOfWeekIndex;
final int firstDayOffset =
computeFirstDayOffset(year, month, firstDayOfWeekIndex);
final bool showDates = datePickerLayoutSettings.showPrevMonthEnd;
if (showDates) {
int prevMonth = month - 1;
if (prevMonth < 1) prevMonth = 12;
int prevYear = prevMonth == 12 ? year - 1 : year;
int daysInPrevMonth = DatePickerUtils.getDaysInMonth(prevYear, prevMonth);
List<Widget> days = List.generate(firstDayOffset, (index) => index)
.reversed
.map((i) => daysInPrevMonth - i)
.map((day) => _buildCell(prevYear, prevMonth, day))
.toList();
result = days;
} else {
result = List.generate(firstDayOffset, (_) => const SizedBox.shrink());
}
return result;
}
List<Widget> _buildMonthCells(MaterialLocalizations localizations) {
List<Widget> result = [];
final int year = displayedMonth.year;
final int month = displayedMonth.month;
final int daysInMonth = DatePickerUtils.getDaysInMonth(year, month);
for (int i = 1; i <= daysInMonth; i += 1) {
Widget dayWidget = _buildCell(year, month, i);
result.add(dayWidget);
}
return result;
}
List<Widget> _buildCellsAfterEnd(MaterialLocalizations localizations) {
List<Widget> result = [];
final bool showDates = datePickerLayoutSettings.showNextMonthStart;
if (!showDates) return result;
final int year = displayedMonth.year;
final int month = displayedMonth.month;
final int firstDayOfWeekIndex = datePickerStyles.firstDayOfeWeekIndex ??
localizations.firstDayOfWeekIndex;
final int firstDayOffset =
computeFirstDayOffset(year, month, firstDayOfWeekIndex);
final int daysInMonth = DatePickerUtils.getDaysInMonth(year, month);
final int totalFilledDays = firstDayOffset + daysInMonth;
int reminder = totalFilledDays % 7;
if (reminder == 0) return result;
final int emptyCellsNum = 7 - reminder;
int nextMonth = month + 1;
result = List.generate(emptyCellsNum, (i) => i + 1)
.map((day) => _buildCell(year, nextMonth, day))
.toList();
return result;
}
Widget _buildCell(int year, int month, int day) {
DateTime dayToBuild = DateTime(year, month, day);
dayToBuild = _checkDateTime(dayToBuild);
DayType dayType = selectablePicker.getDayType(dayToBuild);
Widget dayWidget = _DayCell(
day: dayToBuild,
currentDate: currentDate,
selectablePicker: selectablePicker,
datePickerStyles: datePickerStyles,
eventDecorationBuilder: eventDecorationBuilder,
localizations: localizations,
);
if (dayType != DayType.disabled) {
dayWidget = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => selectablePicker.onDayTapped(dayToBuild),
child: dayWidget,
);
}
return dayWidget;
}
/// Checks if [DateTime] is same day as [lastDate] or [firstDate]
/// and returns dt corrected (with time of [lastDate] or [firstDate]).
DateTime _checkDateTime(DateTime dt) {
DateTime result = dt;
// If dayToBuild is the first day we need to save original time for it.
if (DatePickerUtils.sameDate(dt, firstDate)) result = firstDate;
// If dayToBuild is the last day we need to save original time for it.
if (DatePickerUtils.sameDate(dt, lastDate)) result = lastDate;
return result;
}
}
class _DayCell extends StatelessWidget {
/// Day for this cell.
final DateTime day;
/// Selection logic.
final ISelectablePicker selectablePicker;
/// Styles what can be customized by user
final DatePickerRangeStyles datePickerStyles;
/// The current date at the time the picker is displayed.
final DateTime currentDate;
/// Builder to get event decoration for each date.
///
/// All event styles are overridden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
final MaterialLocalizations localizations;
const _DayCell(
{Key? key,
required this.day,
required this.selectablePicker,
required this.datePickerStyles,
required this.currentDate,
required this.localizations,
this.eventDecorationBuilder})
: super(key: key);
@override
Widget build(BuildContext context) {
DayType dayType = selectablePicker.getDayType(day);
BoxDecoration? decoration;
TextStyle? itemStyle;
if (dayType != DayType.disabled && dayType != DayType.notSelected) {
itemStyle = _getSelectedTextStyle(dayType);
decoration = _getSelectedDecoration(dayType);
} else if (dayType == DayType.disabled) {
itemStyle = datePickerStyles.disabledDateStyle;
} else if (DatePickerUtils.sameDate(currentDate, day)) {
itemStyle = datePickerStyles.currentDateStyle;
} else {
itemStyle = datePickerStyles.defaultDateTextStyle;
}
// Merges decoration and textStyle with [EventDecoration].
//
// Merges only in cases if [dayType] is DayType.notSelected.
// If day is current day it is also gets event decoration
// instead of decoration for current date.
if (dayType == DayType.notSelected && eventDecorationBuilder != null) {
EventDecoration? eDecoration = eventDecorationBuilder != null
? eventDecorationBuilder!.call(day)
: null;
decoration = eDecoration?.boxDecoration ?? decoration;
itemStyle = eDecoration?.textStyle ?? itemStyle;
}
String semanticLabel = '${localizations.formatDecimal(day.day)}, '
'${localizations.formatFullDate(day)}';
bool daySelected =
dayType != DayType.disabled && dayType != DayType.notSelected;
Widget dayWidget = Container(
decoration: decoration,
child: Center(
child: Semantics(
// We want the day of month to be spoken first irrespective of the
// locale-specific preferences or TextDirection. This is because
// an accessibility user is more likely to be interested in the
// day of month before the rest of the date, as they are looking
// for the day of month. To do that we prepend day of month to the
// formatted full date.
label: semanticLabel,
selected: daySelected,
child: ExcludeSemantics(
child: Text(localizations.formatDecimal(day.day), style: itemStyle),
),
),
),
);
return dayWidget;
}
BoxDecoration? _getSelectedDecoration(DayType dayType) {
BoxDecoration? result;
if (dayType == DayType.single) {
result = datePickerStyles.selectedSingleDateDecoration;
} else if (dayType == DayType.start) {
result = datePickerStyles.selectedPeriodStartDecoration;
} else if (dayType == DayType.end) {
result = datePickerStyles.selectedPeriodLastDecoration;
} else {
result = datePickerStyles.selectedPeriodMiddleDecoration;
}
return result;
}
TextStyle? _getSelectedTextStyle(DayType dayType) {
TextStyle? result;
if (dayType == DayType.single) {
result = datePickerStyles.selectedDateStyle;
} else if (dayType == DayType.start) {
result = datePickerStyles.selectedPeriodStartTextStyle;
} else if (dayType == DayType.end) {
result = datePickerStyles.selectedPeriodEndTextStyle;
} else {
result = datePickerStyles.selectedPeriodMiddleTextStyle;
}
return result;
}
}

View File

@@ -0,0 +1,13 @@
/// Date period.
class DatePeriod {
/// Start of this period.
final DateTime start;
/// End of this period.
final DateTime end;
///
const DatePeriod(this.start, this.end)
: assert(start != null),
assert(end != null);
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
/// Keys for some date picker's widgets.
///
/// Useful for integration tests to find widgets.
class DatePickerKeys {
/// Key for the previous page icon widget.
final Key previousPageIconKey;
/// Key for the next page icon widget.
final Key nextPageIconKey;
/// Key for showing month.
final Key selectedPeriodKeys;
///
DatePickerKeys(
this.previousPageIconKey, this.nextPageIconKey, this.selectedPeriodKeys);
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'date_picker_styles.dart';
///
mixin CommonDatePickerFunctions {
/// Builds widgets showing abbreviated days of week. The first widget in the
/// returned list corresponds to the first day of week for the current locale.
///
/// Examples:
///
/// ```
/// ┌ Sunday is the first day of week in the US (en_US)
/// |
/// S M T W T F S <-- the returned list contains these widgets
/// _ _ _ _ _ 1 2
/// 3 4 5 6 7 8 9
///
/// ┌ But it's Monday in the UK (en_GB)
/// |
/// M T W T F S S <-- the returned list contains these widgets
/// _ _ _ _ 1 2 3
/// 4 5 6 7 8 9 10
/// ```
List<Widget> getDayHeaders(
DayHeaderStyleBuilder headerStyleBuilder,
List<String> narrowWeekdays,
int firstDayOfWeekIndex) {
final List<Widget> result = <Widget>[];
for (int i = firstDayOfWeekIndex; true; i = (i + 1) % 7) {
DayHeaderStyle? headerStyle = headerStyleBuilder(i);
final String weekday = narrowWeekdays[i];
Widget header = ExcludeSemantics(
child: Container(
decoration: headerStyle?.decoration,
child: Center(
child: Text(
weekday,
style: headerStyle?.textStyle
)
),
),
);
result.add(header);
if (i == (firstDayOfWeekIndex - 1) % 7) {
break;
}
}
return result;
}
/// Computes the offset from the first day of week that the first day of the
/// [month] falls on.
///
/// For example, September 1, 2017 falls on a Friday, which in the calendar
/// localized for United States English appears as:
///
/// ```
/// S M T W T F S
/// _ _ _ _ _ 1 2
/// ```
///
/// The offset for the first day of the months is the number of leading blanks
/// in the calendar, i.e. 5.
///
/// The same date localized for the Russian calendar has a different offset,
/// because the first day of week is Monday rather than Sunday:
///
/// ```
/// M T W T F S S
/// _ _ _ _ 1 2 3
/// ```
///
/// So the offset is 4, rather than 5.
///
/// This code consolidates the following:
///
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
/// falling on Monday.
/// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index
/// into the [MaterialLocalizations.narrowWeekdays] list.
/// - [MaterialLocalizations.narrowWeekdays] list provides localized names of
/// days of week, always starting with Sunday and ending with Saturday.
int computeFirstDayOffset(
int year, int month, int firstDayOfWeekFromSunday) {
// 0-based day of week, with 0 representing Monday.
final int weekdayFromMonday = DateTime(year, month).weekday - 1;
// firstDayOfWeekFromSunday recomputed to be Monday-based
final int firstDayOfWeekFromMonday = (firstDayOfWeekFromSunday - 1) % 7;
// Number of days between the first day of week appearing on the calendar,
// and the day corresponding to the 1-st of the month.
return (weekdayFromMonday - firstDayOfWeekFromMonday) % 7;
}
}

View File

@@ -0,0 +1,386 @@
import 'package:flutter/material.dart';
import 'range_picker.dart';
import 'week_picker.dart';
/// 0 points to Sunday, and 6 points to Saturday.
typedef DayHeaderStyleBuilder = DayHeaderStyle? Function(int dayOfTheWeek);
/// Common styles for date pickers.
///
/// To define more styles for date pickers which allow select some range
/// (e.g. [RangePicker], [WeekPicker]) use [DatePickerRangeStyles].
@immutable
class DatePickerStyles {
/// Styles for title of displayed period
/// (e.g. month for day picker and year for month picker).
final TextStyle? displayedPeriodTitle;
/// Style for the number of current date.
final TextStyle? currentDateStyle;
/// Style for the numbers of disabled dates.
final TextStyle? disabledDateStyle;
/// Style for the number of selected date.
final TextStyle? selectedDateStyle;
/// Used for date which is neither current nor disabled nor selected.
final TextStyle? defaultDateTextStyle;
/// Day cell decoration for selected date in case only one date is selected.
final BoxDecoration? selectedSingleDateDecoration;
/// Style for the day header.
///
/// If you need to customize day header's style depends on day of the week
/// use [dayHeaderStyleBuilder] instead.
final DayHeaderStyle? dayHeaderStyle;
/// Builder to customize styles for day headers depends on day of the week.
/// Where 0 points to Sunday and 6 points to Saturday.
///
/// Builder must return not null value for every weekday from 0 to 6.
///
/// If styles should be the same for any day of the week
/// use [dayHeaderStyle] instead.
final DayHeaderStyleBuilder? dayHeaderStyleBuilder;
/// Widget which will be shown left side of the shown page title.
/// User goes to previous data period by click on it.
final Widget prevIcon;
/// Widget which will be shown right side of the shown page title.
/// User goes to next data period by click on it.
final Widget nextIcon;
/// Index of the first day of week, where 0 points to Sunday, and 6 points to
/// Saturday. Must not be less 0 or more then 6.
///
/// Can be null. In this case value from current locale will be used.
final int? firstDayOfeWeekIndex;
/// Styles for date picker.
DatePickerStyles({
this.displayedPeriodTitle,
this.currentDateStyle,
this.disabledDateStyle,
this.selectedDateStyle,
this.selectedSingleDateDecoration,
this.defaultDateTextStyle,
this.dayHeaderStyleBuilder,
this.dayHeaderStyle,
this.firstDayOfeWeekIndex,
this.prevIcon = const Icon(Icons.chevron_left),
this.nextIcon = const Icon(Icons.chevron_right)
}) : assert(!(dayHeaderStyle != null && dayHeaderStyleBuilder != null),
"Should be only one from: dayHeaderStyleBuilder, dayHeaderStyle."),
assert(dayHeaderStyleBuilder == null
|| _validateDayHeaderStyleBuilder(dayHeaderStyleBuilder),
"dayHeaderStyleBuilder must return not null value from every weekday "
"(from 0 to 6)."),
assert(_validateFirstDayOfWeek(firstDayOfeWeekIndex),
"firstDayOfeWeekIndex must be null or in correct range (from 0 to 6).");
/// Return new [DatePickerStyles] object where fields
/// with null values set with defaults from theme.
DatePickerStyles fulfillWithTheme(ThemeData theme) {
Color accentColor = theme.accentColor;
TextStyle? _displayedPeriodTitle =
displayedPeriodTitle ?? theme.textTheme.subtitle1;
TextStyle? _currentDateStyle = currentDateStyle ??
theme.textTheme.bodyText1?.copyWith(color: theme.accentColor);
TextStyle? _disabledDateStyle = disabledDateStyle ??
theme.textTheme.bodyText2?.copyWith(color: theme.disabledColor);
TextStyle? _selectedDateStyle =
selectedDateStyle ?? theme.accentTextTheme.bodyText1;
TextStyle? _defaultDateTextStyle =
defaultDateTextStyle ?? theme.textTheme.bodyText2;
BoxDecoration _selectedSingleDateDecoration =
selectedSingleDateDecoration ??
BoxDecoration(
color: accentColor,
borderRadius: const BorderRadius.all(Radius.circular(10.0)));
DayHeaderStyle? _dayHeaderStyle = dayHeaderStyle;
if (dayHeaderStyleBuilder == null && _dayHeaderStyle == null) {
_dayHeaderStyle = DayHeaderStyle(textStyle: theme.textTheme.caption);
}
return DatePickerStyles(
disabledDateStyle: _disabledDateStyle,
currentDateStyle: _currentDateStyle,
displayedPeriodTitle: _displayedPeriodTitle,
selectedDateStyle: _selectedDateStyle,
selectedSingleDateDecoration: _selectedSingleDateDecoration,
defaultDateTextStyle: _defaultDateTextStyle,
dayHeaderStyle: _dayHeaderStyle,
dayHeaderStyleBuilder: dayHeaderStyleBuilder,
nextIcon: nextIcon,
prevIcon: prevIcon
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is DatePickerStyles
&& other.displayedPeriodTitle == displayedPeriodTitle
&& other.currentDateStyle == currentDateStyle
&& other.disabledDateStyle == disabledDateStyle
&& other.selectedDateStyle == selectedDateStyle
&& other.defaultDateTextStyle == defaultDateTextStyle
&& other.selectedSingleDateDecoration == selectedSingleDateDecoration
&& other.dayHeaderStyle == dayHeaderStyle
&& other.dayHeaderStyleBuilder == dayHeaderStyleBuilder
&& other.prevIcon == prevIcon
&& other.nextIcon == nextIcon
&& other.firstDayOfeWeekIndex == firstDayOfeWeekIndex;
}
@override
int get hashCode =>
hashValues(
displayedPeriodTitle,
currentDateStyle,
disabledDateStyle,
selectedDateStyle,
defaultDateTextStyle,
selectedSingleDateDecoration,
dayHeaderStyle,
dayHeaderStyleBuilder,
prevIcon,
nextIcon,
firstDayOfeWeekIndex
);
static bool _validateDayHeaderStyleBuilder(DayHeaderStyleBuilder builder) {
List<int> weekdays = const [0, 1, 2, 3, 4, 5, 6];
// ignore: avoid_types_on_closure_parameters
bool valid = weekdays.every((int weekday) => builder(weekday) != null);
return valid;
}
static bool _validateFirstDayOfWeek(int? index) {
if (index == null) return true;
bool valid = index >= 0 && index <= 6;
return valid;
}
}
/// Styles for date pickers which allow select some range
/// (e.g. RangePicker, WeekPicker).
@immutable
class DatePickerRangeStyles extends DatePickerStyles {
/// Decoration for the first date of the selected range.
final BoxDecoration? selectedPeriodStartDecoration;
/// Text style for the first date of the selected range.
///
/// If null - default [DatePickerStyles.selectedDateStyle] will be used.
final TextStyle? selectedPeriodStartTextStyle;
/// Decoration for the last date of the selected range.
final BoxDecoration? selectedPeriodLastDecoration;
/// Text style for the last date of the selected range.
///
/// If null - default [DatePickerStyles.selectedDateStyle] will be used.
final TextStyle? selectedPeriodEndTextStyle;
/// Decoration for the date of the selected range
/// which is not first date and not end date of this range.
///
/// If there is only one date selected
/// [DatePickerStyles.selectedSingleDateDecoration] will be used.
final BoxDecoration? selectedPeriodMiddleDecoration;
/// Text style for the middle date of the selected range.
///
/// If null - default [DatePickerStyles.selectedDateStyle] will be used.
final TextStyle? selectedPeriodMiddleTextStyle;
/// Return new [DatePickerRangeStyles] object
/// where fields with null values set with defaults from given theme.
@override
DatePickerRangeStyles fulfillWithTheme(ThemeData theme) {
Color accentColor = theme.accentColor;
DatePickerStyles commonStyles = super.fulfillWithTheme(theme);
final BoxDecoration _selectedPeriodStartDecoration =
selectedPeriodStartDecoration ??
BoxDecoration(
color: accentColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10.0),
bottomLeft: Radius.circular(10.0)),
);
final BoxDecoration _selectedPeriodLastDecoration =
selectedPeriodLastDecoration ??
BoxDecoration(
color: accentColor,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10.0),
bottomRight: Radius.circular(10.0)),
);
final BoxDecoration _selectedPeriodMiddleDecoration =
selectedPeriodMiddleDecoration ??
BoxDecoration(
color: accentColor,
shape: BoxShape.rectangle,
);
final TextStyle? _selectedPeriodStartTextStyle =
selectedPeriodStartTextStyle ?? commonStyles.selectedDateStyle;
final TextStyle? _selectedPeriodMiddleTextStyle =
selectedPeriodMiddleTextStyle ?? commonStyles.selectedDateStyle;
final TextStyle? _selectedPeriodEndTextStyle =
selectedPeriodEndTextStyle ?? commonStyles.selectedDateStyle;
return DatePickerRangeStyles(
disabledDateStyle: commonStyles.disabledDateStyle,
currentDateStyle: commonStyles.currentDateStyle,
displayedPeriodTitle: commonStyles.displayedPeriodTitle,
selectedDateStyle: commonStyles.selectedDateStyle,
selectedSingleDateDecoration: commonStyles.selectedSingleDateDecoration,
defaultDateTextStyle: commonStyles.defaultDateTextStyle,
dayHeaderStyle: commonStyles.dayHeaderStyle,
dayHeaderStyleBuilder: commonStyles.dayHeaderStyleBuilder,
firstDayOfWeekIndex: firstDayOfeWeekIndex,
selectedPeriodStartDecoration: _selectedPeriodStartDecoration,
selectedPeriodMiddleDecoration: _selectedPeriodMiddleDecoration,
selectedPeriodLastDecoration: _selectedPeriodLastDecoration,
selectedPeriodStartTextStyle: _selectedPeriodStartTextStyle,
selectedPeriodMiddleTextStyle: _selectedPeriodMiddleTextStyle,
selectedPeriodEndTextStyle: _selectedPeriodEndTextStyle,
);
}
/// Styles for the pickers that allows to select range ([RangePicker],
/// [WeekPicker]).
DatePickerRangeStyles({
displayedPeriodTitle,
currentDateStyle,
disabledDateStyle,
selectedDateStyle,
selectedSingleDateDecoration,
defaultDateTextStyle,
dayHeaderStyle,
dayHeaderStyleBuilder,
Widget nextIcon = const Icon(Icons.chevron_right),
Widget prevIcon = const Icon(Icons.chevron_left),
firstDayOfWeekIndex,
this.selectedPeriodLastDecoration,
this.selectedPeriodMiddleDecoration,
this.selectedPeriodStartDecoration,
this.selectedPeriodStartTextStyle,
this.selectedPeriodMiddleTextStyle,
this.selectedPeriodEndTextStyle,
}) : super(
displayedPeriodTitle: displayedPeriodTitle,
currentDateStyle: currentDateStyle,
disabledDateStyle: disabledDateStyle,
selectedDateStyle: selectedDateStyle,
selectedSingleDateDecoration: selectedSingleDateDecoration,
defaultDateTextStyle: defaultDateTextStyle,
dayHeaderStyle: dayHeaderStyle,
dayHeaderStyleBuilder: dayHeaderStyleBuilder,
nextIcon: nextIcon,
prevIcon: prevIcon,
firstDayOfeWeekIndex: firstDayOfWeekIndex
);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is DatePickerRangeStyles
&& other.selectedPeriodStartDecoration == selectedPeriodStartDecoration
&& other.selectedPeriodStartTextStyle == selectedPeriodStartTextStyle
&& other.selectedPeriodLastDecoration == selectedPeriodLastDecoration
&& other.selectedPeriodEndTextStyle == selectedPeriodEndTextStyle
&& other.selectedPeriodMiddleDecoration ==selectedPeriodMiddleDecoration
&& other.selectedPeriodMiddleTextStyle == selectedPeriodMiddleTextStyle
&& other.displayedPeriodTitle == displayedPeriodTitle
&& other.currentDateStyle == currentDateStyle
&& other.disabledDateStyle == disabledDateStyle
&& other.selectedDateStyle == selectedDateStyle
&& other.defaultDateTextStyle == defaultDateTextStyle
&& other.selectedSingleDateDecoration == selectedSingleDateDecoration
&& other.dayHeaderStyle == dayHeaderStyle
&& other.dayHeaderStyleBuilder == dayHeaderStyleBuilder
&& other.prevIcon == prevIcon
&& other.nextIcon == nextIcon
&& other.firstDayOfeWeekIndex == firstDayOfeWeekIndex;
}
@override
int get hashCode =>
hashValues(
selectedPeriodStartDecoration,
selectedPeriodStartTextStyle,
selectedPeriodLastDecoration,
selectedPeriodEndTextStyle,
selectedPeriodMiddleDecoration,
selectedPeriodMiddleTextStyle,
displayedPeriodTitle,
currentDateStyle,
disabledDateStyle,
selectedDateStyle,
defaultDateTextStyle,
selectedSingleDateDecoration,
dayHeaderStyle,
dayHeaderStyleBuilder,
prevIcon,
nextIcon,
firstDayOfeWeekIndex
);
}
/// User styles for the day header in date picker.
@immutable
class DayHeaderStyle {
/// If null - textTheme.caption from the Theme will be used.
final TextStyle? textStyle;
/// If null - no decoration will be applied for the day header;
final BoxDecoration? decoration;
/// Creates styles for the day headers in date pickers.
///
/// See also:
/// * [DatePickerStyles.dayHeaderStyleBuilder]
const DayHeaderStyle({
this.textStyle,
this.decoration
});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is DayHeaderStyle
&& other.textStyle == textStyle
&& other.decoration == decoration;
}
@override
int get hashCode => hashValues(
textStyle,
decoration
);
}

View File

@@ -0,0 +1,363 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'basic_day_based_widget.dart';
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'day_based_changeable_picker_presenter.dart';
import 'day_picker_selection.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
import 'month_navigation_row.dart';
import 'semantic_sorting.dart';
import 'typedefs.dart';
import 'utils.dart';
const Locale _defaultLocale = Locale('en', 'US');
/// Date picker based on [DayBasedPicker] picker (for days, weeks, ranges).
/// Allows select previous/next month.
class DayBasedChangeablePicker<T> extends StatefulWidget {
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DayPickerSelection selection;
/// Called when the user picks a new T.
final ValueChanged<T> onChanged;
/// Called when the error was thrown after user selection.
final OnSelectionError? onSelectionError;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Date for defining what month should be shown initially.
///
/// Default value is [selection.earliest].
final DateTime initiallyShowDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Styles what can be customized by user
final DatePickerRangeStyles datePickerStyles;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Logic for date selections.
final ISelectablePicker<T> selectablePicker;
/// Builder to get event decoration for each date.
///
/// All event styles are overridden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
/// Called when the user changes the month
final ValueChanged<DateTime>? onMonthChanged;
/// Create picker with option to change month.
DayBasedChangeablePicker(
{Key? key,
required this.selection,
required this.onChanged,
required this.firstDate,
required this.lastDate,
required this.datePickerLayoutSettings,
required this.datePickerStyles,
required this.selectablePicker,
DateTime? initiallyShownDate,
this.datePickerKeys,
this.onSelectionError,
this.eventDecorationBuilder,
this.onMonthChanged})
: initiallyShowDate = initiallyShownDate ?? selection.earliest,
super(key: key);
@override
State<DayBasedChangeablePicker<T>> createState() =>
_DayBasedChangeablePickerState<T>();
}
// todo: Check initial selection and call onSelectionError in case it has error
// todo: (ISelectablePicker.curSelectionIsCorrupted);
class _DayBasedChangeablePickerState<T>
extends State<DayBasedChangeablePicker<T>> {
DateTime _todayDate = DateTime.now();
Locale curLocale = _defaultLocale;
MaterialLocalizations localizations = _defaultLocalizations;
PageController _dayPickerController = PageController();
// Styles from widget fulfilled with current Theme.
DatePickerRangeStyles _resultStyles = DatePickerRangeStyles();
DayBasedChangeablePickerPresenter _presenter = _defaultPresenter;
Timer? _timer;
StreamSubscription<T>? _changesSubscription;
@override
void initState() {
super.initState();
// Initially display the pre-selected date.
final int monthPage = _getInitPage();
_dayPickerController = PageController(initialPage: monthPage);
_changesSubscription = widget.selectablePicker.onUpdate
.listen((newSelectedDate) => widget.onChanged(newSelectedDate))
..onError((e) => widget.onSelectionError != null
? widget.onSelectionError!.call(e)
: print(e.toString()));
_updateCurrentDate();
_initPresenter();
}
@override
void didUpdateWidget(DayBasedChangeablePicker<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.datePickerStyles != oldWidget.datePickerStyles) {
final ThemeData theme = Theme.of(context);
_resultStyles = widget.datePickerStyles.fulfillWithTheme(theme);
}
if (widget.selectablePicker != oldWidget.selectablePicker) {
_changesSubscription = widget.selectablePicker.onUpdate
.listen((newSelectedDate) => widget.onChanged(newSelectedDate))
..onError((e) => widget.onSelectionError != null
? widget.onSelectionError!.call(e)
: print(e.toString()));
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
curLocale = Localizations.localeOf(context);
MaterialLocalizations? curLocalizations =
Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
if (curLocalizations != null && localizations != curLocalizations) {
localizations = curLocalizations;
_initPresenter();
}
final ThemeData theme = Theme.of(context);
_resultStyles = widget.datePickerStyles.fulfillWithTheme(theme);
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return SizedBox(
width: widget.datePickerLayoutSettings.monthPickerPortraitWidth,
height: widget.datePickerLayoutSettings.maxDayPickerHeight,
child: Column(
children: <Widget>[
widget.datePickerLayoutSettings.hideMonthNavigationRow
? const SizedBox()
: SizedBox(
height: widget.datePickerLayoutSettings.dayPickerRowHeight,
child: Padding(
//match _DayPicker main layout padding
padding: widget.datePickerLayoutSettings.contentPadding,
child: _buildMonthNavigationRow()),
),
Expanded(
child: Semantics(
sortKey: MonthPickerSortKey.calendar,
child: _buildDayPickerPageView(),
),
),
],
));
}
@override
void dispose() {
_timer?.cancel();
_dayPickerController.dispose();
_changesSubscription?.cancel();
widget.selectablePicker.dispose();
_presenter.dispose();
super.dispose();
}
void _updateCurrentDate() {
_todayDate = DateTime.now();
final DateTime tomorrow =
DateTime(_todayDate.year, _todayDate.month, _todayDate.day + 1);
Duration timeUntilTomorrow = tomorrow.difference(_todayDate);
timeUntilTomorrow +=
const Duration(seconds: 1); // so we don't miss it by rounding
_timer?.cancel();
_timer = Timer(timeUntilTomorrow, () {
setState(_updateCurrentDate);
});
}
// ignore: prefer_expression_function_bodies
Widget _buildMonthNavigationRow() {
return StreamBuilder<DayBasedChangeablePickerState>(
stream: _presenter.data,
initialData: _presenter.lastVal,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
DayBasedChangeablePickerState state = snapshot.data!;
return MonthNavigationRow(
previousPageIconKey: widget.datePickerKeys?.previousPageIconKey,
nextPageIconKey: widget.datePickerKeys?.nextPageIconKey,
previousMonthTooltip: state.prevTooltip,
nextMonthTooltip: state.nextTooltip,
onPreviousMonthTapped:
state.isFirstMonth ? null : _presenter.gotoPrevMonth,
onNextMonthTapped:
state.isLastMonth ? null : _presenter.gotoNextMonth,
title: Text(
state.curMonthDis,
key: widget.datePickerKeys?.selectedPeriodKeys,
style: _resultStyles.displayedPeriodTitle,
),
nextIcon: widget.datePickerStyles.nextIcon,
prevIcon: widget.datePickerStyles.prevIcon,
);
});
}
Widget _buildDayPickerPageView() => PageView.builder(
controller: _dayPickerController,
scrollDirection: Axis.horizontal,
itemCount:
DatePickerUtils.monthDelta(widget.firstDate, widget.lastDate) + 1,
itemBuilder: _buildCalendar,
onPageChanged: _handleMonthPageChanged,
);
Widget _buildCalendar(BuildContext context, int index) {
final DateTime targetDate =
DatePickerUtils.addMonthsToMonthDate(widget.firstDate, index);
return DayBasedPicker(
key: ValueKey<DateTime>(targetDate),
selectablePicker: widget.selectablePicker,
currentDate: _todayDate,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
displayedMonth: targetDate,
datePickerLayoutSettings: widget.datePickerLayoutSettings,
selectedPeriodKey: widget.datePickerKeys?.selectedPeriodKeys,
datePickerStyles: _resultStyles,
eventDecorationBuilder: widget.eventDecorationBuilder,
localizations: localizations,
);
}
// Returns appropriate date to be shown for init.
// If [widget.initiallyShowDate] is out of bounds [widget.firstDate]
// - [widget.lastDate], nearest bound will be used.
DateTime _getCheckedInitialDate() {
DateTime initiallyShowDateChecked = widget.initiallyShowDate;
if (initiallyShowDateChecked.isBefore(widget.firstDate)) {
initiallyShowDateChecked = widget.firstDate;
}
if (initiallyShowDateChecked.isAfter(widget.lastDate)) {
initiallyShowDateChecked = widget.lastDate;
}
return initiallyShowDateChecked;
}
int _getInitPage() {
final initialDate = _getCheckedInitialDate();
int initPage = DatePickerUtils.monthDelta(
widget.firstDate, initialDate
);
return initPage;
}
void _initPresenter() {
_presenter.dispose();
_presenter = DayBasedChangeablePickerPresenter(
firstDate: widget.firstDate,
lastDate: widget.lastDate,
localizations: localizations,
showPrevMonthDates: widget.datePickerLayoutSettings.showPrevMonthEnd,
showNextMonthDates: widget.datePickerLayoutSettings.showNextMonthStart,
firstDayOfWeekIndex: widget.datePickerStyles.firstDayOfeWeekIndex);
_presenter.data.listen(_onStateChanged);
// date used to define what month should be shown
DateTime initSelection = _getCheckedInitialDate();
// Give information about initial selection to presenter.
// It should be done after first frame when PageView is already created.
// Otherwise event from presenter will cause a error.
WidgetsBinding.instance!.addPostFrameCallback((_) {
_presenter.setSelectedDate(initSelection);
});
}
void _onStateChanged(DayBasedChangeablePickerState newState) {
DateTime newMonth = newState.currentMonth;
final int monthPage =
DatePickerUtils.monthDelta(widget.firstDate, newMonth);
_dayPickerController.animateToPage(monthPage,
duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
}
void _handleMonthPageChanged(int monthPage) {
DateTime firstMonth = widget.firstDate;
DateTime newMonth = DateTime(firstMonth.year, firstMonth.month + monthPage);
_presenter.changeMonth(newMonth);
widget.onMonthChanged?.call(newMonth);
}
static MaterialLocalizations get _defaultLocalizations =>
MaterialLocalizationEn(
twoDigitZeroPaddedFormat:
intl.NumberFormat('00', _defaultLocale.toString()),
fullYearFormat: intl.DateFormat.y(_defaultLocale.toString()),
longDateFormat: intl.DateFormat.yMMMMEEEEd(_defaultLocale.toString()),
shortMonthDayFormat: intl.DateFormat.MMMd(_defaultLocale.toString()),
decimalFormat:
intl.NumberFormat.decimalPattern(_defaultLocale.toString()),
shortDateFormat: intl.DateFormat.yMMMd(_defaultLocale.toString()),
mediumDateFormat: intl.DateFormat.MMMEd(_defaultLocale.toString()),
compactDateFormat: intl.DateFormat.yMd(_defaultLocale.toString()),
yearMonthFormat: intl.DateFormat.yMMMM(_defaultLocale.toString()),
);
static DayBasedChangeablePickerPresenter get _defaultPresenter =>
DayBasedChangeablePickerPresenter(
firstDate: DateTime.now(),
lastDate: DateTime.now(),
localizations: _defaultLocalizations,
showPrevMonthDates: false,
showNextMonthDates: false,
firstDayOfWeekIndex: 1);
}

View File

@@ -0,0 +1,177 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'day_based_changable_picker.dart';
import 'utils.dart';
/// Presenter for [DayBasedChangeablePicker] to handle month changes.
class DayBasedChangeablePickerPresenter {
/// First date user can select.
final DateTime firstDate;
/// Last date user can select.
final DateTime lastDate;
/// Localization.
final MaterialLocalizations localizations;
/// If empty day cells before 1st day of showing month should be filled with
/// date from the last week of the previous month.
final bool showPrevMonthDates;
/// If empty day cells after last day of showing month should be filled with
/// date from the first week of the next month.
final bool showNextMonthDates;
/// Index of the first day in week.
/// 0 is Sunday, 6 is Saturday.
final int firstDayOfWeekIndex;
/// View model stream for the [DayBasedChangeablePicker].
Stream<DayBasedChangeablePickerState> get data => _controller.stream;
/// Last view model state of the [DayBasedChangeablePicker].
DayBasedChangeablePickerState? get lastVal => _lastVal;
/// Creates presenter to use for [DayBasedChangeablePicker].
DayBasedChangeablePickerPresenter({
required this.firstDate,
required this.lastDate,
required this.localizations,
required this.showPrevMonthDates,
required this.showNextMonthDates,
int? firstDayOfWeekIndex
}): firstDayOfWeekIndex = firstDayOfWeekIndex
?? localizations.firstDayOfWeekIndex;
/// Update state according to the [selectedDate] if it needs.
void setSelectedDate(DateTime selectedDate) {
// bool firstAndLastNotNull = _firstShownDate != null
// && _lastShownDate != null;
//
// bool selectedOnCurPage = firstAndLastNotNull
// && !selectedDate.isBefore(_firstShownDate)
// && !selectedDate.isAfter(_lastShownDate);
// if (selectedOnCurPage) return;
changeMonth(selectedDate);
}
/// Update state to show previous month.
void gotoPrevMonth() {
DateTime oldCur = _lastVal!.currentMonth;
DateTime newCurDate = DateTime(oldCur.year, oldCur.month - 1);
changeMonth(newCurDate);
}
/// Update state to show next month.
void gotoNextMonth() {
DateTime oldCur = _lastVal!.currentMonth;
DateTime newCurDate = DateTime(oldCur.year, oldCur.month + 1);
changeMonth(newCurDate);
}
/// Update state to change month to the [newMonth].
void changeMonth(DateTime newMonth) {
bool sameMonth = _lastVal != null
&& DatePickerUtils.sameMonth(_lastVal!.currentMonth, newMonth);
if (sameMonth) return;
int monthPage = DatePickerUtils.monthDelta(firstDate, newMonth);
DateTime prevMonth = DatePickerUtils
.addMonthsToMonthDate(firstDate, monthPage - 1);
DateTime curMonth = DatePickerUtils
.addMonthsToMonthDate(firstDate, monthPage);
DateTime nextMonth = DatePickerUtils
.addMonthsToMonthDate(firstDate, monthPage + 1);
String prevMonthStr = localizations.formatMonthYear(prevMonth);
String curMonthStr = localizations.formatMonthYear(curMonth);
String nextMonthStr = localizations.formatMonthYear(nextMonth);
bool isFirstMonth = DatePickerUtils.sameMonth(curMonth, firstDate);
bool isLastMonth = DatePickerUtils.sameMonth(curMonth, lastDate);
String? prevTooltip = isFirstMonth
? null
: "${localizations.previousMonthTooltip} $prevMonthStr";
String? nextTooltip = isLastMonth
? null
: "${localizations.nextMonthTooltip} $nextMonthStr";
DayBasedChangeablePickerState newState = DayBasedChangeablePickerState(
currentMonth: curMonth,
curMonthDis: curMonthStr,
prevMonthDis: prevMonthStr,
nextMonthDis: nextMonthStr,
prevTooltip: prevTooltip,
nextTooltip: nextTooltip,
isFirstMonth: isFirstMonth,
isLastMonth: isLastMonth
);
_updateState(newState);
}
/// Closes controller.
void dispose () {
_controller.close();
}
void _updateState(DayBasedChangeablePickerState newState) {
_lastVal = newState;
_controller.add(newState);
}
final StreamController<DayBasedChangeablePickerState> _controller =
StreamController.broadcast();
DayBasedChangeablePickerState? _lastVal;
}
/// View Model for the [DayBasedChangeablePicker].
class DayBasedChangeablePickerState {
/// Display name of the current month.
final String curMonthDis;
/// Display name of the previous month.
final String prevMonthDis;
/// Display name of the next month.
final String nextMonthDis;
/// Tooltip for the previous month icon.
final String? prevTooltip;
/// Tooltip for the next month icon.
final String? nextTooltip;
/// Tooltip for the current month icon.
final DateTime currentMonth;
/// If selected month is the month contains last date user can select.
final bool isLastMonth;
/// If selected month is the month contains first date user can select.
final bool isFirstMonth;
/// Creates view model for the [DayBasedChangeablePicker].
DayBasedChangeablePickerState({
required this.curMonthDis,
required this.prevMonthDis,
required this.nextMonthDis,
required this.currentMonth,
required this.isLastMonth,
required this.isFirstMonth,
this.prevTooltip,
this.nextTooltip,
});
}

View File

@@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'day_based_changable_picker.dart';
import 'day_picker_selection.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
/// Date picker for selection one day.
class DayPicker<T extends Object> extends StatelessWidget {
DayPicker._({Key? key,
required this.onChanged,
required this.firstDate,
required this.lastDate,
required this.selectionLogic,
required this.selection,
this.initiallyShowDate,
this.datePickerLayoutSettings = const DatePickerLayoutSettings(),
this.datePickerStyles,
this.datePickerKeys,
this.selectableDayPredicate,
this.eventDecorationBuilder,
this.onMonthChanged}) : super(key: key);
/// Creates a day picker where only one single day can be selected.
///
/// See also:
/// * [DayPicker.multi] - day picker where many single days can be selected.
static DayPicker<DateTime> single({
Key? key,
required DateTime selectedDate,
required ValueChanged<DateTime> onChanged,
required DateTime firstDate,
required DateTime lastDate,
DatePickerLayoutSettings datePickerLayoutSettings
= const DatePickerLayoutSettings(),
DateTime? initiallyShowDate,
DatePickerRangeStyles? datePickerStyles,
DatePickerKeys? datePickerKeys,
SelectableDayPredicate? selectableDayPredicate,
EventDecorationBuilder? eventDecorationBuilder,
ValueChanged<DateTime>? onMonthChanged
})
{
assert(!firstDate.isAfter(lastDate));
assert(!lastDate.isBefore(firstDate));
assert(!selectedDate.isBefore(firstDate));
assert(!selectedDate.isAfter(lastDate));
assert(initiallyShowDate == null
|| !initiallyShowDate.isAfter(lastDate));
assert(initiallyShowDate == null
|| !initiallyShowDate.isBefore(firstDate));
final selection = DayPickerSingleSelection(selectedDate);
final selectionLogic = DaySelectable(
selectedDate, firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
return DayPicker<DateTime>._(
onChanged: onChanged,
firstDate: firstDate,
lastDate: lastDate,
initiallyShowDate: initiallyShowDate,
selectionLogic: selectionLogic,
selection: selection,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
selectableDayPredicate: selectableDayPredicate,
datePickerKeys: datePickerKeys,
datePickerStyles: datePickerStyles,
datePickerLayoutSettings: datePickerLayoutSettings,
);
}
/// Creates a day picker where many single days can be selected.
///
/// See also:
/// * [DayPicker.single] - day picker where only one single day
/// can be selected.
static DayPicker<List<DateTime>> multi({Key? key,
required List<DateTime> selectedDates,
required ValueChanged<List<DateTime>> onChanged,
required DateTime firstDate,
required DateTime lastDate,
DatePickerLayoutSettings datePickerLayoutSettings
= const DatePickerLayoutSettings(),
DateTime? initiallyShowDate,
DatePickerRangeStyles? datePickerStyles,
DatePickerKeys? datePickerKeys,
SelectableDayPredicate? selectableDayPredicate,
EventDecorationBuilder? eventDecorationBuilder,
ValueChanged<DateTime>? onMonthChanged})
{
assert(!firstDate.isAfter(lastDate));
assert(!lastDate.isBefore(firstDate));
assert(initiallyShowDate == null
|| !initiallyShowDate.isAfter(lastDate));
assert(initiallyShowDate == null
|| !initiallyShowDate.isBefore(lastDate));
final selection = DayPickerMultiSelection(selectedDates);
final selectionLogic = DayMultiSelectable(
selectedDates, firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
return DayPicker<List<DateTime>>._(
onChanged: onChanged,
firstDate: firstDate,
lastDate: lastDate,
initiallyShowDate: initiallyShowDate,
selectionLogic: selectionLogic,
selection: selection,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
selectableDayPredicate: selectableDayPredicate,
datePickerKeys: datePickerKeys,
datePickerStyles: datePickerStyles,
datePickerLayoutSettings: datePickerLayoutSettings,
);
}
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DayPickerSelection selection;
/// Called when the user picks a day.
final ValueChanged<T> onChanged;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Date for defining what month should be shown initially.
///
/// In case of null earliest of the [selection] will be shown.
final DateTime? initiallyShowDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Styles what can be customized by user
final DatePickerRangeStyles? datePickerStyles;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Function returns if day can be selected or not.
///
/// If null
final SelectableDayPredicate? selectableDayPredicate;
/// Builder to get event decoration for each date.
///
/// All event styles are overriden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
// Called when the user changes the month.
/// New DateTime object represents first day of new month and 00:00 time.
final ValueChanged<DateTime>? onMonthChanged;
/// Logic to handle user's selections.
final ISelectablePicker<T> selectionLogic;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return DayBasedChangeablePicker<T>(
selectablePicker: selectionLogic,
selection: selection,
firstDate: firstDate,
lastDate: lastDate,
initiallyShownDate: initiallyShowDate,
onChanged: onChanged,
datePickerLayoutSettings: datePickerLayoutSettings,
datePickerStyles: datePickerStyles ?? DatePickerRangeStyles(),
datePickerKeys: datePickerKeys,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
);
}
}

View File

@@ -0,0 +1,120 @@
import 'date_period.dart';
import 'utils.dart';
/// Base class for day based pickers selection.
abstract class DayPickerSelection {
/// If this is before [dateTime].
bool isBefore(DateTime dateTime);
/// If this is after [dateTime].
bool isAfter(DateTime dateTime);
/// Returns earliest [DateTime] in this selection.
DateTime get earliest;
/// If this selection is empty.
bool get isEmpty;
/// If this selection is not empty.
bool get isNotEmpty;
/// Constructor to allow children to have constant constructor.
const DayPickerSelection();
}
/// Selection with only one selected date.
///
/// See also:
/// * [DayPickerMultiSelection] - selection with one or many single dates.
/// * [DayPickerRangeSelection] - date period selection.
class DayPickerSingleSelection extends DayPickerSelection {
/// Selected date.
final DateTime selectedDate;
/// Creates selection with only one selected date.
const DayPickerSingleSelection(this.selectedDate)
: assert(selectedDate != null);
@override
bool isAfter(DateTime dateTime) => selectedDate.isAfter(dateTime);
@override
bool isBefore(DateTime dateTime) => selectedDate.isAfter(dateTime);
@override
DateTime get earliest => selectedDate;
@override
bool get isEmpty => selectedDate == null;
@override
bool get isNotEmpty => selectedDate != null;
}
/// Selection with one or many single dates.
///
/// See also:
/// * [DayPickerSingleSelection] - selection with only one selected date.
/// * [DayPickerRangeSelection] - date period selection.
class DayPickerMultiSelection extends DayPickerSelection {
/// List of the selected dates.
final List<DateTime> selectedDates;
/// Selection with one or many single dates.
DayPickerMultiSelection(this.selectedDates)
: assert(selectedDates != null);
@override
bool isAfter(DateTime dateTime)
=> selectedDates.every((d) => d.isAfter(dateTime));
@override
bool isBefore(DateTime dateTime)
=> selectedDates.every((d) => d.isBefore(dateTime));
@override
DateTime get earliest => DatePickerUtils.getEarliestFromList(selectedDates);
@override
bool get isEmpty => selectedDates.isEmpty;
@override
bool get isNotEmpty => selectedDates.isNotEmpty;
}
/// Date period selection.
///
/// See also:
/// * [DayPickerSingleSelection] - selection with only one selected date.
/// * [DayPickerMultiSelection] - selection with one or many single dates.
class DayPickerRangeSelection extends DayPickerSelection {
/// Selected period.
final DatePeriod selectedRange;
/// Date period selection.
const DayPickerRangeSelection(this.selectedRange)
: assert(selectedRange != null);
@override
DateTime get earliest => selectedRange.start;
@override
bool isAfter(DateTime dateTime) => selectedRange.start.isAfter(dateTime);
@override
bool isBefore(DateTime dateTime) => selectedRange.end.isBefore(dateTime);
@override
bool get isEmpty => selectedRange == null;
@override
bool get isNotEmpty => selectedRange != null;
}

View File

@@ -0,0 +1,20 @@
/// Type of the day in day based date picker.
enum DayType {
/// start of the selected period
start,
/// middle of the selected period
middle,
/// end of the selected period
end,
/// selected single day
single,
/// disabled day
disabled,
/// not selected day (but not disabled)
notSelected
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/widgets.dart';
import 'day_picker.dart';
import 'range_picker.dart';
import 'week_picker.dart';
/// Signature for function which is used to set set specific decoration for
/// some days in [DayPicker], [WeekPicker] and [RangePicker].
///
/// See also:
/// * [DayPicker.eventDecorationBuilder]
/// * [WeekPicker.eventDecorationBuilder]
/// * [RangePicker.eventDecorationBuilder]
typedef EventDecorationBuilder = EventDecoration? Function(DateTime date);
/// Class to store styles for event (specific day in the date picker).
@immutable
class EventDecoration {
/// Cell decoration for the specific day in the date picker (event).
final BoxDecoration? boxDecoration;
/// Style for number of the specific day in the date picker (event).
final TextStyle? textStyle;
/// Creates decoration for special day.
///
/// Used for [EventDecorationBuilder] function which is usually passed to
/// [DayPicker.eventDecorationBuilder], [WeekPicker.eventDecorationBuilder]
/// and [RangePicker.eventDecorationBuilder] to set specific decoration for
/// some days.
const EventDecoration({this.boxDecoration, this.textStyle});
}

View File

@@ -0,0 +1,537 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'date_period.dart';
import 'day_picker.dart' as day_picker;
import 'day_type.dart';
import 'range_picker.dart';
import 'unselectable_period_error.dart';
import 'utils.dart';
/// Interface for selection logic of the different date pickers.
///
/// T - is selection type.
abstract class ISelectablePicker<T> {
/// The earliest date the user is permitted to pick.
/// (only year, month and day matter, time doesn't matter)
final DateTime firstDate;
/// The latest date the user is permitted to pick.
/// (only year, month and day matter, time doesn't matter)
final DateTime lastDate;
/// Function returns if day can be selected or not.
final SelectableDayPredicate _selectableDayPredicate;
/// StreamController for new selection (T).
@protected
StreamController<T> onUpdateController = StreamController<T>.broadcast();
/// Stream with new selected (T) event.
///
/// Throws [UnselectablePeriodException]
/// if there is any custom disabled date in selected.
Stream<T> get onUpdate => onUpdateController.stream;
/// Constructor with required fields that used in non-abstract methods
/// ([isDisabled]).
ISelectablePicker(this.firstDate, this.lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: _selectableDayPredicate =
selectableDayPredicate ?? _defaultSelectableDayPredicate;
/// If current selection exists and includes day/days that can't be selected
/// according to the [_selectableDayPredicate]'
bool get curSelectionIsCorrupted;
/// Returns [DayType] for given [day].
DayType getDayType(DateTime day);
/// Call when user tap on the day cell.
void onDayTapped(DateTime selectedDate);
/// Returns if given day is disabled.
///
/// Returns weather given day before the beginning of the [firstDate]
/// or after the end of the [lastDate].
///
/// If [_selectableDayPredicate] is set checks it as well.
@protected
bool isDisabled(DateTime day) {
final DateTime beginOfTheFirstDay =
DatePickerUtils.startOfTheDay(firstDate);
final DateTime endOfTheLastDay = DatePickerUtils.endOfTheDay(lastDate);
final bool customDisabled =
_selectableDayPredicate != null ? !_selectableDayPredicate(day) : false;
return day.isAfter(endOfTheLastDay) ||
day.isBefore(beginOfTheFirstDay) ||
customDisabled;
}
/// Closes [onUpdateController].
/// After it [onUpdateController] can't get new events.
void dispose() {
onUpdateController.close();
}
static bool _defaultSelectableDayPredicate(_) => true;
}
/// Selection logic for WeekPicker.
class WeekSelectable extends ISelectablePicker<DatePeriod> {
/// Initialized in ctor body.
late DateTime _firstDayOfSelectedWeek;
/// Initialized in ctor body.
late DateTime _lastDayOfSelectedWeek;
// It is int from 0 to 6 where 0 points to Sunday and 6 points to Saturday.
// According to MaterialLocalization.firstDayOfWeekIndex.
final int _firstDayOfWeekIndex;
@override
bool get curSelectionIsCorrupted => _checkCurSelection();
/// Creates selection logic for WeekPicker.
///
/// Entire week will be selected if
/// * it is between [firstDate] and [lastDate]
/// * it doesn't include unselectable days according to the
/// [selectableDayPredicate]
///
/// If one or more days of the week are before [firstDate]
/// first selection date will be the same as [firstDate].
///
/// If one or more days of the week are after [lastDate]
/// last selection date will be the same as [lastDate].
///
/// If one or more days of week are not selectable according to the
/// [selectableDayPredicate] nothing will be returned as selection
/// but [UnselectablePeriodException] will be thrown.
WeekSelectable(DateTime selectedDate, this._firstDayOfWeekIndex,
DateTime firstDate, DateTime lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: super(firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate) {
DatePeriod selectedWeek = _getNewSelectedPeriod(selectedDate);
_firstDayOfSelectedWeek = selectedWeek.start;
_lastDayOfSelectedWeek = selectedWeek.end;
_checkCurSelection();
}
@override
DayType getDayType(DateTime date) {
DayType result;
DatePeriod selectedPeriod =
DatePeriod(_firstDayOfSelectedWeek, _lastDayOfSelectedWeek);
bool selectedPeriodIsBroken =
_disabledDatesInPeriod(selectedPeriod).isNotEmpty;
if (isDisabled(date)) {
result = DayType.disabled;
} else if (_isDaySelected(date) && !selectedPeriodIsBroken) {
DateTime firstNotDisabledDayOfSelectedWeek =
_firstDayOfSelectedWeek.isBefore(firstDate)
? firstDate
: _firstDayOfSelectedWeek;
DateTime lastNotDisabledDayOfSelectedWeek =
_lastDayOfSelectedWeek.isAfter(lastDate)
? lastDate
: _lastDayOfSelectedWeek;
if (DatePickerUtils.sameDate(date, firstNotDisabledDayOfSelectedWeek) &&
DatePickerUtils.sameDate(date, lastNotDisabledDayOfSelectedWeek)) {
result = DayType.single;
} else if (DatePickerUtils.sameDate(date, _firstDayOfSelectedWeek) ||
DatePickerUtils.sameDate(date, firstDate)) {
result = DayType.start;
} else if (DatePickerUtils.sameDate(date, _lastDayOfSelectedWeek) ||
DatePickerUtils.sameDate(date, lastDate)) {
result = DayType.end;
} else {
result = DayType.middle;
}
} else {
result = DayType.notSelected;
}
return result;
}
@override
void onDayTapped(DateTime selectedDate) {
DatePeriod newPeriod = _getNewSelectedPeriod(selectedDate);
List<DateTime> customDisabledDays = _disabledDatesInPeriod(newPeriod);
customDisabledDays.isEmpty
? onUpdateController.add(newPeriod)
: onUpdateController.addError(
UnselectablePeriodException(customDisabledDays, newPeriod));
}
// Returns new selected period according to tapped date.
// Doesn't check custom disabled days.
// You have to check it separately if it needs.
DatePeriod _getNewSelectedPeriod(DateTime tappedDay) {
DatePeriod newPeriod;
DateTime firstDayOfTappedWeek =
DatePickerUtils.getFirstDayOfWeek(tappedDay, _firstDayOfWeekIndex);
DateTime lastDayOfTappedWeek =
DatePickerUtils.getLastDayOfWeek(tappedDay, _firstDayOfWeekIndex);
DateTime firstNotDisabledDayOfSelectedWeek =
firstDayOfTappedWeek.isBefore(firstDate)
? firstDate
: firstDayOfTappedWeek;
DateTime lastNotDisabledDayOfSelectedWeek =
lastDayOfTappedWeek.isAfter(lastDate) ? lastDate : lastDayOfTappedWeek;
newPeriod = DatePeriod(
firstNotDisabledDayOfSelectedWeek, lastNotDisabledDayOfSelectedWeek);
return newPeriod;
}
bool _isDaySelected(DateTime date) {
DateTime startOfTheStartDay =
DatePickerUtils.startOfTheDay(_firstDayOfSelectedWeek);
DateTime endOfTheLastDay =
DatePickerUtils.endOfTheDay(_lastDayOfSelectedWeek);
return !(date.isBefore(startOfTheStartDay) ||
date.isAfter(endOfTheLastDay));
}
List<DateTime> _disabledDatesInPeriod(DatePeriod period) {
List<DateTime> result = <DateTime>[];
var date = period.start;
while (!date.isAfter(period.end)) {
if (isDisabled(date)) result.add(date);
date = date.add(Duration(days: 1));
}
return result;
}
// Returns if current selection contains disabled dates.
// Returns false if there is no any selection.
bool _checkCurSelection() {
bool noSelection =
_firstDayOfSelectedWeek == null || _lastDayOfSelectedWeek == null;
if (noSelection) return false;
DatePeriod selectedPeriod =
DatePeriod(_firstDayOfSelectedWeek, _lastDayOfSelectedWeek);
List<DateTime> disabledDates = _disabledDatesInPeriod(selectedPeriod);
bool selectedPeriodIsBroken = disabledDates.isNotEmpty;
return selectedPeriodIsBroken;
}
}
/// Selection logic for [day_picker.DayPicker].
class DaySelectable extends ISelectablePicker<DateTime> {
/// Currently selected date.
DateTime selectedDate;
@override
bool get curSelectionIsCorrupted => _checkCurSelection();
/// Creates selection logic for [day_picker.DayPicker].
///
/// Every day can be selected if it is between [firstDate] and [lastDate]
/// and not unselectable according to the [selectableDayPredicate].
///
/// If day is not selectable according to the [selectableDayPredicate]
/// nothing will be returned as selection
/// but [UnselectablePeriodException] will be thrown.
DaySelectable(this.selectedDate, DateTime firstDate, DateTime lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: super(firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
@override
DayType getDayType(DateTime date) {
DayType result;
if (isDisabled(date)) {
result = DayType.disabled;
} else if (_isDaySelected(date)) {
result = DayType.single;
} else {
result = DayType.notSelected;
}
return result;
}
@override
void onDayTapped(DateTime selectedDate) {
DateTime newSelected = DatePickerUtils.sameDate(firstDate, selectedDate)
? selectedDate
: DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
onUpdateController.add(newSelected);
}
bool _isDaySelected(DateTime date) =>
DatePickerUtils.sameDate(date, selectedDate);
// Returns if current selection is disabled
// according to the [_selectableDayPredicate].
//
// Returns false if there is no any selection.
bool _checkCurSelection() {
if (selectedDate == null) return false;
bool selectedIsBroken = _selectableDayPredicate(selectedDate);
return selectedIsBroken;
}
}
/// Selection logic for [day_picker.DayPicker] where many single days can be
/// selected.
class DayMultiSelectable extends ISelectablePicker<List<DateTime>> {
/// Currently selected dates.
List<DateTime> selectedDates;
/// Creates selection logic for [day_picker.DayPicker].
///
/// Every day can be selected if it is between [firstDate] and [lastDate]
/// and not unselectable according to the [selectableDayPredicate].
///
/// If day is not selectable according to the [selectableDayPredicate]
/// nothing will be returned as selection
/// but [UnselectablePeriodException] will be thrown.
DayMultiSelectable(this.selectedDates, DateTime firstDate, DateTime lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: super(firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
@override
bool get curSelectionIsCorrupted => _checkCurSelection();
@override
DayType getDayType(DateTime date) {
DayType result;
if (isDisabled(date)) {
result = DayType.disabled;
} else if (_isDaySelected(date)) {
result = DayType.single;
} else {
result = DayType.notSelected;
}
return result;
}
@override
void onDayTapped(DateTime selectedDate) {
bool alreadyExist =
selectedDates.any((d) => DatePickerUtils.sameDate(d, selectedDate));
if (alreadyExist) {
List<DateTime> newSelectedDates = List.from(selectedDates)
..removeWhere((d) => DatePickerUtils.sameDate(d, selectedDate));
onUpdateController.add(newSelectedDates);
} else {
DateTime newSelected = DatePickerUtils.sameDate(firstDate, selectedDate)
? selectedDate
: DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
List<DateTime> newSelectedDates = List.from(selectedDates)
..add(newSelected);
onUpdateController.add(newSelectedDates);
}
}
bool _isDaySelected(DateTime date) =>
selectedDates.any((d) => DatePickerUtils.sameDate(date, d));
// Returns if current selection is disabled
// according to the [_selectableDayPredicate].
//
// Returns false if there is no any selection.
bool _checkCurSelection() {
if (selectedDates == null || selectedDates.isEmpty) return false;
bool selectedIsBroken = selectedDates.every(_selectableDayPredicate);
return selectedIsBroken;
}
}
/// Selection logic for [RangePicker].
class RangeSelectable extends ISelectablePicker<DatePeriod> {
/// Initially selected period.
DatePeriod selectedPeriod;
@override
bool get curSelectionIsCorrupted => _checkCurSelection();
/// Creates selection logic for [RangePicker].
///
/// Period can be selected if
/// * it is between [firstDate] and [lastDate]
/// * it doesn't include unselectable days according to the
/// [selectableDayPredicate]
///
///
/// If one or more days of the period are not selectable according to the
/// [selectableDayPredicate] nothing will be returned as selection
/// but [UnselectablePeriodException] will be thrown.
RangeSelectable(this.selectedPeriod, DateTime firstDate, DateTime lastDate,
{SelectableDayPredicate? selectableDayPredicate})
: super(firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
@override
DayType getDayType(DateTime date) {
DayType result;
bool selectedPeriodIsBroken =
_disabledDatesInPeriod(selectedPeriod).isNotEmpty;
if (isDisabled(date)) {
result = DayType.disabled;
} else if (_isDaySelected(date) && !selectedPeriodIsBroken) {
if (DatePickerUtils.sameDate(date, selectedPeriod.start) &&
DatePickerUtils.sameDate(date, selectedPeriod.end)) {
result = DayType.single;
} else if (DatePickerUtils.sameDate(date, selectedPeriod.start) ||
DatePickerUtils.sameDate(date, firstDate)) {
result = DayType.start;
} else if (DatePickerUtils.sameDate(date, selectedPeriod.end) ||
DatePickerUtils.sameDate(date, lastDate)) {
result = DayType.end;
} else {
result = DayType.middle;
}
} else {
result = DayType.notSelected;
}
return result;
}
@override
void onDayTapped(DateTime selectedDate) {
DatePeriod newPeriod = _getNewSelectedPeriod(selectedDate);
List<DateTime> customDisabledDays = _disabledDatesInPeriod(newPeriod);
customDisabledDays.isEmpty
? onUpdateController.add(newPeriod)
: onUpdateController.addError(
UnselectablePeriodException(customDisabledDays, newPeriod));
}
// Returns new selected period according to tapped date.
DatePeriod _getNewSelectedPeriod(DateTime tappedDate) {
// check if was selected only one date and we should generate period
bool sameDate =
DatePickerUtils.sameDate(selectedPeriod.start, selectedPeriod.end);
DatePeriod newPeriod;
// Was selected one-day-period.
// With new user tap will be generated 2 dates as a period.
if (sameDate) {
// if user tap on the already selected single day
bool selectedAlreadySelectedDay =
DatePickerUtils.sameDate(tappedDate, selectedPeriod.end);
bool isSelectedFirstDay = DatePickerUtils.sameDate(tappedDate, firstDate);
bool isSelectedLastDay = DatePickerUtils.sameDate(tappedDate, lastDate);
if (selectedAlreadySelectedDay) {
if (isSelectedFirstDay && isSelectedLastDay) {
newPeriod = DatePeriod(firstDate, lastDate);
} else if (isSelectedFirstDay) {
newPeriod =
DatePeriod(firstDate, DatePickerUtils.endOfTheDay(firstDate));
} else if (isSelectedLastDay) {
newPeriod =
DatePeriod(DatePickerUtils.startOfTheDay(lastDate), lastDate);
} else {
newPeriod = DatePeriod(DatePickerUtils.startOfTheDay(tappedDate),
DatePickerUtils.endOfTheDay(tappedDate));
}
} else {
DateTime startOfTheSelectedDay =
DatePickerUtils.startOfTheDay(selectedPeriod.start);
if (!tappedDate.isAfter(startOfTheSelectedDay)) {
newPeriod = DatePickerUtils.sameDate(tappedDate, firstDate)
? DatePeriod(firstDate, selectedPeriod.end)
: DatePeriod(DatePickerUtils.startOfTheDay(tappedDate),
selectedPeriod.end);
} else {
newPeriod = DatePickerUtils.sameDate(tappedDate, lastDate)
? DatePeriod(selectedPeriod.start, lastDate)
: DatePeriod(selectedPeriod.start,
DatePickerUtils.endOfTheDay(tappedDate));
}
}
// Was selected 2 dates as a period.
// With new user tap new one-day-period will be generated.
} else {
bool sameAsFirst = DatePickerUtils.sameDate(tappedDate, firstDate);
bool sameAsLast = DatePickerUtils.sameDate(tappedDate, lastDate);
if (sameAsFirst && sameAsLast) {
newPeriod = DatePeriod(firstDate, lastDate);
} else if (sameAsFirst) {
newPeriod =
DatePeriod(firstDate, DatePickerUtils.endOfTheDay(firstDate));
} else if (sameAsLast) {
newPeriod =
DatePeriod(DatePickerUtils.startOfTheDay(tappedDate), lastDate);
} else {
newPeriod = DatePeriod(DatePickerUtils.startOfTheDay(tappedDate),
DatePickerUtils.endOfTheDay(tappedDate));
}
}
return newPeriod;
}
// Returns if current selection contains disabled dates.
// Returns false if there is no any selection.
bool _checkCurSelection() {
if (selectedPeriod == null) return false;
List<DateTime> disabledDates = _disabledDatesInPeriod(selectedPeriod);
bool selectedPeriodIsBroken = disabledDates.isNotEmpty;
return selectedPeriodIsBroken;
}
List<DateTime> _disabledDatesInPeriod(DatePeriod period) {
List<DateTime> result = <DateTime>[];
var date = period.start;
while (!date.isAfter(period.end)) {
if (isDisabled(date)) result.add(date);
date = date.add(Duration(days: 1));
}
return result;
}
bool _isDaySelected(DateTime date) {
DateTime startOfTheStartDay =
DatePickerUtils.startOfTheDay(selectedPeriod.start);
DateTime endOfTheLastDay = DatePickerUtils.endOfTheDay(selectedPeriod.end);
return !(date.isBefore(startOfTheStartDay) ||
date.isAfter(endOfTheLastDay));
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// Icon button widget built different
/// depends on [MaterialApp] or [CupertinoApp] ancestor.
class IconBtn extends StatelessWidget {
/// Widget to use inside button.
///
/// Typically [Icon] widget.
final Widget icon;
/// Function called when user tap on the button.
///
/// Can be null. In this case button will be disabled.
final VoidCallback? onTap;
/// Tooltip for button.
///
/// Applied only for material style buttons.
/// It means only if widget has [MaterialApp] ancestor.
final String? tooltip;
/// Creates button with [icon] different
/// depends on [MaterialApp] or [CupertinoApp] ancestor.
const IconBtn({
Key? key,
required this.icon,
this.onTap,
this.tooltip
}) : super(key: key);
@override
Widget build(BuildContext context) {
bool isMaterial = Material.of(context) != null;
return isMaterial
? _materialBtn()
: _cupertinoBtn();
}
Widget _cupertinoBtn() =>
CupertinoButton(
padding: const EdgeInsets.all(0.0),
child: icon,
onPressed: onTap,
);
Widget _materialBtn() =>
IconButton(
icon: icon,
tooltip: tooltip ?? "",
onPressed: onTap,
);
}

View File

@@ -0,0 +1,115 @@
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'day_picker.dart';
import 'month_picker.dart';
import 'range_picker.dart';
import 'week_picker.dart';
// layout defaults
const Duration _kPageScrollDuration = Duration(milliseconds: 200);
const double _kDayPickerRowHeight = 42.0;
const int _kMaxDayPickerRowCount = 6; // A 31 day month that starts on Saturday.
const double _kMonthPickerPortraitWidth = 330.0;
const EdgeInsetsGeometry _kContentPadding =
EdgeInsets.symmetric(horizontal: 8.0);
/// Settings for the layout of the [DayPicker], [WeekPicker], [RangePicker]
/// and [MonthPicker].
class DatePickerLayoutSettings {
/// Duration for scroll to previous or next page.
final Duration pagesScrollDuration;
/// Determines the scroll physics of a date picker widget.
///
/// Can be null. In this case default physics for [ScrollView] will be used.
final ScrollPhysics? scrollPhysics;
/// Height of the one row in picker including headers.
///
/// Default is [_kDayPickerRowHeight].
final double dayPickerRowHeight;
/// Width of the day based pickers.
final double monthPickerPortraitWidth;
///
final int maxDayPickerRowCount;
/// Padding for the entire picker.
final EdgeInsetsGeometry contentPadding;
/// If the first dates from the next month should be shown
/// to complete last week of the selected month.
///
/// false by default.
final bool showNextMonthStart;
/// If the last dates from the previous month should be shown
/// to complete first week of the selected month.
///
/// false by default.
final bool showPrevMonthEnd;
/// Hide Month navigation row
/// false by default.
final bool hideMonthNavigationRow;
/// Grid delegate for the picker according to [dayPickerRowHeight] and
/// [maxDayPickerRowCount].
SliverGridDelegate get dayPickerGridDelegate =>
_DayPickerGridDelegate(dayPickerRowHeight, maxDayPickerRowCount);
/// Maximum height of the day based picker according to [dayPickerRowHeight]
/// and [maxDayPickerRowCount].
///
/// Two extra rows:
/// one for the day-of-week header and one for the month header.
double get maxDayPickerHeight =>
dayPickerRowHeight * (maxDayPickerRowCount + 2);
/// Creates layout settings for the date picker.
///
/// Usually used in [DayPicker], [WeekPicker], [RangePicker]
/// and [MonthPicker].
const DatePickerLayoutSettings({
this.pagesScrollDuration = _kPageScrollDuration,
this.dayPickerRowHeight = _kDayPickerRowHeight,
this.monthPickerPortraitWidth = _kMonthPickerPortraitWidth,
this.maxDayPickerRowCount = _kMaxDayPickerRowCount,
this.contentPadding = _kContentPadding,
this.showNextMonthStart = false,
this.showPrevMonthEnd = false,
this.hideMonthNavigationRow = false,
this.scrollPhysics
});
}
class _DayPickerGridDelegate extends SliverGridDelegate {
final double _dayPickerRowHeight;
final int _maxDayPickerRowCount;
const _DayPickerGridDelegate(
this._dayPickerRowHeight, this._maxDayPickerRowCount);
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
const int columnCount = DateTime.daysPerWeek;
final double tileWidth = constraints.crossAxisExtent / columnCount;
final double tileHeight = math.min(_dayPickerRowHeight,
constraints.viewportMainAxisExtent / (_maxDayPickerRowCount + 1));
return SliverGridRegularTileLayout(
crossAxisCount: columnCount,
mainAxisStride: tileHeight,
crossAxisStride: tileWidth,
childMainAxisExtent: tileHeight,
childCrossAxisExtent: tileWidth,
reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
);
}
@override
bool shouldRelayout(SliverGridDelegate oldDelegate) => false;
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'day_picker.dart' as day_picker;
import 'icon_btn.dart';
import 'range_picker.dart';
import 'semantic_sorting.dart';
import 'week_picker.dart';
/// Month navigation widget for day based date pickers like
/// [day_picker.DayPicker],
/// [WeekPicker],
/// [RangePicker].
///
/// It is row with [title] of showing month in the center and icons to selects
/// previous and next month around it.
class MonthNavigationRow extends StatelessWidget {
/// Key for previous page icon.
///
/// Can be useful in integration tests to find icon.
final Key? previousPageIconKey;
/// Key for next page icon.
///
/// Can be useful in integration tests to find icon.
final Key? nextPageIconKey;
/// Function called when [nextIcon] is tapped.
final VoidCallback? onNextMonthTapped;
/// Function called when [prevIcon] is tapped.
final VoidCallback? onPreviousMonthTapped;
/// Tooltip for the [nextIcon].
final String? nextMonthTooltip;
/// Tooltip for the [prevIcon].
final String? previousMonthTooltip;
/// Widget to use at the end of this row (after title).
final Widget nextIcon;
/// Widget to use at the beginning of this row (before title).
final Widget prevIcon;
/// Usually [Text] widget.
final Widget? title;
/// Creates month navigation row.
const MonthNavigationRow({
Key? key,
this.previousPageIconKey,
this.nextPageIconKey,
this.onNextMonthTapped,
this.onPreviousMonthTapped,
this.nextMonthTooltip,
this.previousMonthTooltip,
this.title,
required this.nextIcon,
required this.prevIcon
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Semantics(
sortKey: MonthPickerSortKey.previousMonth,
child: IconBtn(
key: previousPageIconKey,
icon: prevIcon,
tooltip: previousMonthTooltip,
onTap: onPreviousMonthTapped,
),
),
Expanded(
child: Container(
alignment: Alignment.center,
child: Center(
child: ExcludeSemantics(
child: title,
),
),
),
),
Semantics(
sortKey: MonthPickerSortKey.nextMonth,
child: IconBtn(
key: nextPageIconKey,
icon: nextIcon,
tooltip: nextMonthTooltip,
onTap: onNextMonthTapped,
),
),
],
);
}
}

View File

@@ -0,0 +1,440 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'layout_settings.dart';
import 'semantic_sorting.dart';
import 'utils.dart';
const Locale _defaultLocale = Locale('en', 'US');
/// Month picker widget.
class MonthPicker extends StatefulWidget {
/// Month picker widget.
MonthPicker(
{Key? key,
required this.selectedDate,
required this.onChanged,
required this.firstDate,
required this.lastDate,
this.datePickerLayoutSettings = const DatePickerLayoutSettings(),
this.datePickerKeys,
required this.datePickerStyles})
: assert(!firstDate.isAfter(lastDate)),
assert(!selectedDate.isBefore(firstDate)),
assert(!selectedDate.isAfter(lastDate)),
super(key: key);
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DateTime selectedDate;
/// Called when the user picks a month.
final ValueChanged<DateTime> onChanged;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Styles what can be customized by user
final DatePickerStyles datePickerStyles;
@override
State<StatefulWidget> createState() => _MonthPickerState();
}
class _MonthPickerState extends State<MonthPicker> {
PageController _monthPickerController = PageController();
Locale locale = _defaultLocale;
MaterialLocalizations localizations = _defaultLocalizations;
TextDirection textDirection = TextDirection.ltr;
DateTime _todayDate = DateTime.now();
DateTime _previousYearDate = DateTime(DateTime.now().year - 1);
DateTime _nextYearDate = DateTime(DateTime.now().year + 1);
DateTime _currentDisplayedYearDate = DateTime.now();
Timer? _timer;
/// True if the earliest allowable year is displayed.
bool get _isDisplayingFirstYear =>
!_currentDisplayedYearDate.isAfter(DateTime(widget.firstDate.year));
/// True if the latest allowable year is displayed.
bool get _isDisplayingLastYear =>
!_currentDisplayedYearDate.isBefore(DateTime(widget.lastDate.year));
@override
void initState() {
super.initState();
// Initially display the pre-selected date.
final int yearPage =
DatePickerUtils.yearDelta(widget.firstDate, widget.selectedDate);
_monthPickerController.dispose();
_monthPickerController = PageController(initialPage: yearPage);
_handleYearPageChanged(yearPage);
_updateCurrentDate();
}
@override
void didUpdateWidget(MonthPicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedDate != oldWidget.selectedDate) {
final int yearPage =
DatePickerUtils.yearDelta(widget.firstDate, widget.selectedDate);
_monthPickerController = PageController(initialPage: yearPage);
_handleYearPageChanged(yearPage);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
try {
locale = Localizations.localeOf(context);
MaterialLocalizations? curLocalizations =
Localizations.of<MaterialLocalizations>(
context, MaterialLocalizations);
if (curLocalizations != null && localizations != curLocalizations) {
localizations = curLocalizations;
}
textDirection = Directionality.of(context);
// No MaterialLocalizations or Directionality or Locale was found
// and ".of" method throws error
// trying to cast null to MaterialLocalizations.
} on TypeError catch (_) {}
}
void _updateCurrentDate() {
_todayDate = DateTime.now();
final DateTime tomorrow =
DateTime(_todayDate.year, _todayDate.month, _todayDate.day + 1);
Duration timeUntilTomorrow = tomorrow.difference(_todayDate);
timeUntilTomorrow +=
const Duration(seconds: 1); // so we don't miss it by rounding
_timer?.cancel();
_timer = Timer(timeUntilTomorrow, () {
setState(_updateCurrentDate);
});
}
/// Add years to a year truncated date.
DateTime _addYearsToYearDate(DateTime yearDate, int yearsToAdd) =>
DateTime(yearDate.year + yearsToAdd);
Widget _buildItems(BuildContext context, int index) {
final DateTime year = _addYearsToYearDate(widget.firstDate, index);
final ThemeData theme = Theme.of(context);
DatePickerStyles styles = widget.datePickerStyles;
styles = styles.fulfillWithTheme(theme);
return _MonthPicker(
key: ValueKey<DateTime>(year),
selectedDate: widget.selectedDate,
currentDate: _todayDate,
onChanged: widget.onChanged,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
datePickerLayoutSettings: widget.datePickerLayoutSettings,
displayedYear: year,
selectedPeriodKey: widget.datePickerKeys?.selectedPeriodKeys,
datePickerStyles: styles,
locale: locale,
localizations: localizations,
);
}
void _handleNextYear() {
if (!_isDisplayingLastYear) {
String yearStr = localizations.formatYear(_nextYearDate);
SemanticsService.announce(yearStr, textDirection);
_monthPickerController.nextPage(
duration: widget.datePickerLayoutSettings.pagesScrollDuration,
curve: Curves.ease);
}
}
void _handlePreviousYear() {
if (!_isDisplayingFirstYear) {
String yearStr = localizations.formatYear(_previousYearDate);
SemanticsService.announce(yearStr, textDirection);
_monthPickerController.previousPage(
duration: widget.datePickerLayoutSettings.pagesScrollDuration,
curve: Curves.ease);
}
}
void _handleYearPageChanged(int yearPage) {
setState(() {
_previousYearDate = _addYearsToYearDate(widget.firstDate, yearPage - 1);
_currentDisplayedYearDate =
_addYearsToYearDate(widget.firstDate, yearPage);
_nextYearDate = _addYearsToYearDate(widget.firstDate, yearPage + 1);
});
}
@override
Widget build(BuildContext context) {
int yearsCount =
DatePickerUtils.yearDelta(widget.firstDate, widget.lastDate) + 1;
return SizedBox(
width: widget.datePickerLayoutSettings.monthPickerPortraitWidth,
height: widget.datePickerLayoutSettings.maxDayPickerHeight,
child: Stack(
children: <Widget>[
Semantics(
sortKey: YearPickerSortKey.calendar,
child: PageView.builder(
key: ValueKey<DateTime>(widget.selectedDate),
controller: _monthPickerController,
scrollDirection: Axis.horizontal,
itemCount: yearsCount,
itemBuilder: _buildItems,
onPageChanged: _handleYearPageChanged,
),
),
PositionedDirectional(
top: 0.0,
start: 8.0,
child: Semantics(
sortKey: YearPickerSortKey.previousYear,
child: IconButton(
key: widget.datePickerKeys?.previousPageIconKey,
icon: widget.datePickerStyles.prevIcon,
tooltip: _isDisplayingFirstYear
? null
: '${localizations.formatYear(_previousYearDate)}',
onPressed: _isDisplayingFirstYear ? null : _handlePreviousYear,
),
),
),
PositionedDirectional(
top: 0.0,
end: 8.0,
child: Semantics(
sortKey: YearPickerSortKey.nextYear,
child: IconButton(
key: widget.datePickerKeys?.nextPageIconKey,
icon: widget.datePickerStyles.nextIcon,
tooltip: _isDisplayingLastYear
? null
: '${localizations.formatYear(_nextYearDate)}',
onPressed: _isDisplayingLastYear ? null : _handleNextYear,
),
),
),
],
),
);
}
static MaterialLocalizations get _defaultLocalizations =>
MaterialLocalizationEn(
twoDigitZeroPaddedFormat:
intl.NumberFormat('00', _defaultLocale.toString()),
fullYearFormat: intl.DateFormat.y(_defaultLocale.toString()),
longDateFormat: intl.DateFormat.yMMMMEEEEd(_defaultLocale.toString()),
shortMonthDayFormat: intl.DateFormat.MMMd(_defaultLocale.toString()),
decimalFormat:
intl.NumberFormat.decimalPattern(_defaultLocale.toString()),
shortDateFormat: intl.DateFormat.yMMMd(_defaultLocale.toString()),
mediumDateFormat: intl.DateFormat.MMMEd(_defaultLocale.toString()),
compactDateFormat: intl.DateFormat.yMd(_defaultLocale.toString()),
yearMonthFormat: intl.DateFormat.yMMMM(_defaultLocale.toString()),
);
}
class _MonthPicker extends StatelessWidget {
/// The month whose days are displayed by this picker.
final DateTime displayedYear;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DateTime selectedDate;
/// The current date at the time the picker is displayed.
final DateTime currentDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Called when the user picks a day.
final ValueChanged<DateTime> onChanged;
/// Key fo selected month (useful for integration tests)
final Key? selectedPeriodKey;
/// Styles what can be customized by user
final DatePickerStyles datePickerStyles;
final MaterialLocalizations localizations;
final Locale locale;
_MonthPicker(
{required this.displayedYear,
required this.firstDate,
required this.lastDate,
required this.selectedDate,
required this.currentDate,
required this.onChanged,
required this.datePickerLayoutSettings,
required this.datePickerStyles,
required this.localizations,
required this.locale,
this.selectedPeriodKey,
Key? key})
: assert(!firstDate.isAfter(lastDate)),
assert(selectedDate.isAfter(firstDate) ||
selectedDate.isAtSameMomentAs(firstDate)),
super(key: key);
// We only need to know if month of passed day
// before the month of the firstDate or after the month of the lastDate.
//
// Don't need to compare day and time.
bool _isDisabled(DateTime month) {
DateTime beginningOfTheFirstDateMonth =
DateTime(firstDate.year, firstDate.month);
DateTime endOfTheLastDateMonth = DateTime(lastDate.year, lastDate.month + 1)
.subtract(Duration(microseconds: 1));
return month.isAfter(endOfTheLastDateMonth) ||
month.isBefore(beginningOfTheFirstDateMonth);
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final int monthsInYear = 12;
final int year = displayedYear.year;
final int day = 1;
final List<Widget> labels = <Widget>[];
for (int i = 0; i < monthsInYear; i += 1) {
final int month = i + 1;
final DateTime monthToBuild = DateTime(year, month, day);
final bool disabled = _isDisabled(monthToBuild);
final bool isSelectedMonth =
selectedDate.year == year && selectedDate.month == month;
BoxDecoration? decoration;
TextStyle? itemStyle = themeData.textTheme.bodyText2;
if (isSelectedMonth) {
itemStyle = datePickerStyles.selectedDateStyle;
decoration = datePickerStyles.selectedSingleDateDecoration;
} else if (disabled) {
itemStyle = datePickerStyles.disabledDateStyle;
} else if (currentDate.year == year && currentDate.month == month) {
// The current month gets a different text color.
itemStyle = datePickerStyles.currentDateStyle;
} else {
itemStyle = datePickerStyles.defaultDateTextStyle;
}
String monthStr = _getMonthStr(monthToBuild);
Widget monthWidget = Container(
decoration: decoration,
child: Center(
child: Semantics(
// We want the day of month to be spoken first irrespective of the
// locale-specific preferences or TextDirection. This is because
// an accessibility user is more likely to be interested in the
// day of month before the rest of the date, as they are looking
// for the day of month. To do that we prepend day of month to the
// formatted full date.
label: '${localizations.formatDecimal(month)}, '
'${localizations.formatFullDate(monthToBuild)}',
selected: isSelectedMonth,
child: ExcludeSemantics(
child: Text(monthStr, style: itemStyle),
),
),
),
);
if (!disabled) {
monthWidget = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
DatePickerUtils.sameMonth(firstDate, monthToBuild)
? onChanged(firstDate)
: onChanged(monthToBuild);
},
child: monthWidget,
);
}
labels.add(monthWidget);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
children: <Widget>[
Container(
height: datePickerLayoutSettings.dayPickerRowHeight,
child: Center(
child: ExcludeSemantics(
child: Text(
localizations.formatYear(displayedYear),
key: selectedPeriodKey,
style: datePickerStyles.displayedPeriodTitle,
),
),
),
),
Flexible(
child: GridView.count(
physics: datePickerLayoutSettings.scrollPhysics,
crossAxisCount: 4,
children: labels,
),
),
],
),
);
}
// Returns only month made with intl.DateFormat.MMM() for current [locale].
// We can'r use [localizations] here because MaterialLocalizations doesn't
// provide short month string.
String _getMonthStr(DateTime date) {
String month = intl.DateFormat.MMM(locale.toString()).format(date);
return month;
}
}

View File

@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'date_period.dart';
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'day_based_changable_picker.dart';
import 'day_picker_selection.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
import 'typedefs.dart';
/// Date picker for range selection.
class RangePicker extends StatelessWidget {
/// Creates a range picker.
RangePicker(
{Key? key,
required this.selectedPeriod,
required this.onChanged,
required this.firstDate,
required this.lastDate,
this.initiallyShowDate,
this.datePickerLayoutSettings = const DatePickerLayoutSettings(),
this.datePickerStyles,
this.datePickerKeys,
this.selectableDayPredicate,
this.onSelectionError,
this.eventDecorationBuilder,
this.onMonthChanged})
: assert(!firstDate.isAfter(lastDate)),
assert(!lastDate.isBefore(firstDate)),
assert(!selectedPeriod.start.isBefore(firstDate)),
assert(!selectedPeriod.end.isAfter(lastDate)),
assert(initiallyShowDate == null
|| !initiallyShowDate.isAfter(lastDate)),
assert(initiallyShowDate == null
|| !initiallyShowDate.isBefore(firstDate)),
super(key: key);
/// The currently selected period.
///
/// This date is highlighted in the picker.
final DatePeriod selectedPeriod;
/// Called when the user picks a week.
final ValueChanged<DatePeriod> onChanged;
/// Called when the error was thrown after user selection.
/// (e.g. when user selected a range with one or more days
/// that can't be selected)
final OnSelectionError? onSelectionError;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Date for defining what month should be shown initially.
///
/// In case of null start of the [selectedPeriod] will be shown.
final DateTime? initiallyShowDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Styles what can be customized by user
final DatePickerRangeStyles? datePickerStyles;
/// Function returns if day can be selected or not.
final SelectableDayPredicate? selectableDayPredicate;
/// Builder to get event decoration for each date.
///
/// All event styles are overridden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
/// Called when the user changes the month.
/// New DateTime object represents first day of new month and 00:00 time.
final ValueChanged<DateTime>? onMonthChanged;
@override
Widget build(BuildContext context) {
ISelectablePicker<DatePeriod> rangeSelectablePicker = RangeSelectable(
selectedPeriod, firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
return DayBasedChangeablePicker<DatePeriod>(
selectablePicker: rangeSelectablePicker,
selection: DayPickerRangeSelection(selectedPeriod),
firstDate: firstDate,
lastDate: lastDate,
initiallyShownDate: initiallyShowDate,
onChanged: onChanged,
onSelectionError: onSelectionError,
datePickerLayoutSettings: datePickerLayoutSettings,
datePickerStyles: datePickerStyles ?? DatePickerRangeStyles(),
datePickerKeys: datePickerKeys,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter/semantics.dart';
/// Defines semantic traversal order of the top-level widgets
/// inside the day or week picker.
class MonthPickerSortKey extends OrdinalSortKey {
/// Previous month key.
static const MonthPickerSortKey previousMonth = MonthPickerSortKey(1.0);
/// Next month key.
static const MonthPickerSortKey nextMonth = MonthPickerSortKey(2.0);
/// Calendar key.
static const MonthPickerSortKey calendar = MonthPickerSortKey(3.0);
///
const MonthPickerSortKey(double order) : super(order);
}
/// Defines semantic traversal order of the top-level widgets
/// inside the month picker.
class YearPickerSortKey extends OrdinalSortKey {
/// Previous year key.
static const YearPickerSortKey previousYear = YearPickerSortKey(1.0);
/// Next year key.
static const YearPickerSortKey nextYear = YearPickerSortKey(2.0);
/// Calendar key.
static const YearPickerSortKey calendar = YearPickerSortKey(3.0);
///
const YearPickerSortKey(double order) : super(order);
}

View File

@@ -0,0 +1,11 @@
import 'range_picker.dart';
import 'unselectable_period_error.dart';
import 'week_picker.dart';
/// Signature for function that can be used to handle incorrect selections.
///
/// See also:
/// * [WeekPicker.onSelectionError]
/// * [RangePicker.onSelectionError]
typedef OnSelectionError = void Function(UnselectablePeriodException e);

View File

@@ -0,0 +1,30 @@
import 'date_period.dart';
import 'range_picker.dart';
import 'week_picker.dart';
/// Exception thrown when selected period contains custom disabled days.
class UnselectablePeriodException implements Exception {
/// Dates inside selected period what can't be selected
/// according custom rules.
final List<DateTime> customDisabledDates;
/// Selected period wanted by the user.
final DatePeriod period;
/// Creates exception that stores dates that can not be selected.
///
/// See also:
/// *[WeekPicker.onSelectionError]
/// *[RangePicker.onSelectionError]
UnselectablePeriodException(this.customDisabledDates, this.period);
@override
String toString() =>
"UnselectablePeriodException:"
" ${customDisabledDates.length} dates inside selected period "
"(${period.start} - ${period.end}) "
"can't be selected according custom rules (selectable pridicate). "
"Check 'customDisabledDates' property "
"to get entire list of such dates.";
}

View File

@@ -0,0 +1,251 @@
/// Bunch of useful functions for date pickers.
class DatePickerUtils {
/// Returns if two objects have same year, month and day.
/// Time doesn't matter.
static bool sameDate(DateTime dateTimeOne, DateTime dateTimeTwo) =>
dateTimeOne.year == dateTimeTwo.year &&
dateTimeOne.month == dateTimeTwo.month &&
dateTimeOne.day == dateTimeTwo.day;
/// Returns if two objects have same year and month.
/// Day and time don't matter/
static bool sameMonth(DateTime dateTimeOne, DateTime dateTimeTwo) =>
dateTimeOne.year == dateTimeTwo.year
&& dateTimeOne.month == dateTimeTwo.month;
// Do not use this directly - call getDaysInMonth instead.
static const List<int> _daysInMonth = <int>[
31,
-1,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31
];
/// Returns the number of days in a month, according to the proleptic
/// Gregorian calendar.
///
/// This applies the leap year logic introduced by the Gregorian reforms of
/// 1582. It will not give valid results for dates prior to that time.
static int getDaysInMonth(int year, int month) {
if (month == DateTime.february) {
final bool isLeapYear =
(year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0);
return isLeapYear ? 29 : 28;
}
return _daysInMonth[month - 1];
}
/// Returns number of months between [startDate] and [endDate]
static int monthDelta(DateTime startDate, DateTime endDate) =>
(endDate.year - startDate.year) * 12 +
endDate.month -
startDate.month;
/// Add months to a month truncated date.
static DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) =>
// year is switched automatically if new month > 12
DateTime(monthDate.year, monthDate.month + monthsToAdd);
/// Returns number of years between [startDate] and [endDate]
static int yearDelta(DateTime startDate, DateTime endDate) =>
endDate.year - startDate.year;
/// Returns start of the first day of the week with given day.
///
/// Start of the week calculated using firstDayIndex which is int from 0 to 6
/// where 0 points to Sunday and 6 points to Saturday.
/// (according to MaterialLocalization.firstDayIfWeekIndex)
static DateTime getFirstDayOfWeek(DateTime day, int firstDayIndex) {
// from 1 to 7 where 1 points to Monday and 7 points to Sunday
int weekday = day.weekday;
// to match weekdays where Sunday is 7 not 0
if (firstDayIndex == 0) firstDayIndex = 7;
int diff = weekday - firstDayIndex;
if (diff < 0) diff = 7 + diff;
DateTime firstDayOfWeek = day.subtract(Duration(days: diff));
firstDayOfWeek = startOfTheDay(firstDayOfWeek);
return firstDayOfWeek;
}
/// Returns end of the last day of the week with given day.
///
/// Start of the week calculated using firstDayIndex which is int from 0 to 6
/// where 0 points to Sunday and 6 points to Saturday.
/// (according to MaterialLocalization.firstDayIfWeekIndex)
static DateTime getLastDayOfWeek(DateTime day, int firstDayIndex) {
// from 1 to 7 where 1 points to Monday and 7 points to Sunday
int weekday = day.weekday;
// to match weekdays where Sunday is 7 not 0
if (firstDayIndex == 0) firstDayIndex = 7;
int lastDayIndex = firstDayIndex - 1;
if (lastDayIndex == 0) lastDayIndex = 7;
int diff = lastDayIndex - weekday;
if (diff < 0) diff = 7 + diff;
DateTime lastDayOfWeek = day.add(Duration(days: diff));
lastDayOfWeek = endOfTheDay(lastDayOfWeek);
return lastDayOfWeek;
}
/// Returns end of the given day.
///
/// End time is 1 millisecond before start of the next day.
static DateTime endOfTheDay(DateTime date) {
DateTime tomorrowStart = DateTime(date.year, date.month, date.day + 1);
DateTime result = tomorrowStart.subtract(const Duration(milliseconds: 1));
return result;
}
/// Returns start of the given day.
///
/// Start time is 00:00:00.
static DateTime startOfTheDay(DateTime date) =>
DateTime(date.year, date.month, date.day);
/// Returns first shown date for the [curMonth].
///
/// First shown date is not always 1st day of the [curMonth].
/// It can be day from previous month if [showEndOfPrevMonth] is true.
///
/// If [showEndOfPrevMonth] is true empty day cells before 1st [curMonth]
/// are filled with days from the previous month.
static DateTime firstShownDate({
required DateTime curMonth,
required bool showEndOfPrevMonth,
required int firstDayOfWeekFromSunday}) {
DateTime result = DateTime(curMonth.year, curMonth.month, 1);
if (showEndOfPrevMonth) {
int firstDayOffset = computeFirstDayOffset(curMonth.year, curMonth.month,
firstDayOfWeekFromSunday);
if (firstDayOffset == 0) return result;
int prevMonth = curMonth.month - 1;
if (prevMonth < 1) prevMonth = 12;
int prevYear = prevMonth == 12
? curMonth.year - 1
: curMonth.year;
int daysInPrevMonth = getDaysInMonth(prevYear, prevMonth);
int firstShownDay = daysInPrevMonth - firstDayOffset + 1;
result = DateTime(prevYear, prevMonth, firstShownDay);
}
return result;
}
/// Returns last shown date for the [curMonth].
///
/// Last shown date is not always last day of the [curMonth].
/// It can be day from next month if [showStartNextMonth] is true.
///
/// If [showStartNextMonth] is true empty day cells after last day
/// of [curMonth] are filled with days from the next month.
static DateTime lastShownDate({
required DateTime curMonth,
required bool showStartNextMonth,
required int firstDayOfWeekFromSunday}) {
int daysInCurMonth = getDaysInMonth(curMonth.year, curMonth.month);
DateTime result = DateTime(curMonth.year, curMonth.month, daysInCurMonth);
if (showStartNextMonth) {
int firstDayOffset = computeFirstDayOffset(curMonth.year, curMonth.month,
firstDayOfWeekFromSunday);
int totalDays = firstDayOffset + daysInCurMonth;
int trailingDaysCount = 7 - totalDays % 7;
bool fullWeekTrailing = trailingDaysCount == 7;
if (fullWeekTrailing) return result;
result = DateTime(curMonth.year, curMonth.month + 1, trailingDaysCount);
}
return result;
}
/// Computes the offset from the first day of week that the first day of the
/// [month] falls on.
///
/// For example, September 1, 2017 falls on a Friday, which in the calendar
/// localized for United States English appears as:
///
/// ```
/// S M T W T F S
/// _ _ _ _ _ 1 2
/// ```
///
/// The offset for the first day of the months is the number of leading blanks
/// in the calendar, i.e. 5.
///
/// The same date localized for the Russian calendar has a different offset,
/// because the first day of week is Monday rather than Sunday:
///
/// ```
/// M T W T F S S
/// _ _ _ _ 1 2 3
/// ```
///
/// So the offset is 4, rather than 5.
///
/// This code consolidates the following:
///
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
/// falling on Monday.
/// - MaterialLocalizations.firstDayOfWeekIndex provides a 0-based index
/// into the MaterialLocalizations.narrowWeekdays list.
/// - MaterialLocalizations.narrowWeekdays list provides localized names of
/// days of week, always starting with Sunday and ending with Saturday.
static int computeFirstDayOffset(
int year, int month, int firstDayOfWeekFromSunday) {
// 0-based day of week, with 0 representing Monday.
final int weekdayFromMonday = DateTime(year, month).weekday - 1;
// firstDayOfWeekFromSunday recomputed to be Monday-based
final int firstDayOfWeekFromMonday = (firstDayOfWeekFromSunday - 1) % 7;
// Number of days between the first day of week appearing on the calendar,
// and the day corresponding to the 1-st of the month.
return (weekdayFromMonday - firstDayOfWeekFromMonday) % 7;
}
/// Returns earliest [DateTime] from the list.
///
/// [dates] must not be null.
/// In case it is null, [ArgumentError] will be thrown.
static DateTime getEarliestFromList(List<DateTime> dates) {
ArgumentError.checkNotNull(dates, "dates");
return dates.fold(dates[0], getEarliest);
}
/// Returns earliest [DateTime] from two.
///
/// If two [DateTime]s is the same moment first ([a]) will be return.
static DateTime getEarliest(DateTime a, DateTime b)
=> a.isBefore(b) ? a : b;
}

View File

@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'date_period.dart';
import 'date_picker_keys.dart';
import 'date_picker_styles.dart';
import 'day_based_changable_picker.dart';
import 'day_picker_selection.dart';
import 'day_type.dart';
import 'event_decoration.dart';
import 'i_selectable_picker.dart';
import 'layout_settings.dart';
import 'typedefs.dart';
/// Date picker for selection a week.
class WeekPicker extends StatelessWidget {
/// Creates a month picker.
WeekPicker(
{Key? key,
required this.selectedDate,
required this.onChanged,
required this.firstDate,
required this.lastDate,
this.initiallyShowDate,
this.datePickerLayoutSettings = const DatePickerLayoutSettings(),
this.datePickerStyles,
this.datePickerKeys,
this.selectableDayPredicate,
this.onSelectionError,
this.eventDecorationBuilder,
this.onMonthChanged})
: assert(!firstDate.isAfter(lastDate)),
assert(!lastDate.isBefore(firstDate)),
assert(!selectedDate.isBefore(firstDate)),
assert(!selectedDate.isAfter(lastDate)),
assert(initiallyShowDate == null
|| !initiallyShowDate.isAfter(lastDate)),
assert(initiallyShowDate == null
|| !initiallyShowDate.isBefore(firstDate)),
super(key: key);
/// The currently selected date.
///
/// This date is highlighted in the picker.
final DateTime selectedDate;
/// Called when the user picks a week.
final ValueChanged<DatePeriod> onChanged;
/// Called when the error was thrown after user selection.
/// (e.g. when user selected a week with one or more days
/// what can't be selected)
final OnSelectionError? onSelectionError;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// Date for defining what month should be shown initially.
///
/// In case of null month with earliest date of the selected week
/// will be shown.
final DateTime? initiallyShowDate;
/// Layout settings what can be customized by user
final DatePickerLayoutSettings datePickerLayoutSettings;
/// Some keys useful for integration tests
final DatePickerKeys? datePickerKeys;
/// Styles what can be customized by user
final DatePickerRangeStyles? datePickerStyles;
/// Function returns if day can be selected or not.
final SelectableDayPredicate? selectableDayPredicate;
/// Builder to get event decoration for each date.
///
/// All event styles are overriden by selected styles
/// except days with dayType is [DayType.notSelected].
final EventDecorationBuilder? eventDecorationBuilder;
/// Called when the user changes the month.
/// New DateTime object represents first day of new month and 00:00 time.
final ValueChanged<DateTime>? onMonthChanged;
@override
Widget build(BuildContext context) {
MaterialLocalizations localizations = MaterialLocalizations.of(context);
int firstDayOfWeekIndex = datePickerStyles?.firstDayOfeWeekIndex ??
localizations.firstDayOfWeekIndex;
ISelectablePicker<DatePeriod> weekSelectablePicker = WeekSelectable(
selectedDate, firstDayOfWeekIndex, firstDate, lastDate,
selectableDayPredicate: selectableDayPredicate);
return DayBasedChangeablePicker<DatePeriod>(
selectablePicker: weekSelectablePicker,
// todo: maybe create selection for week
// todo: and change logic here to work with it
selection: DayPickerSingleSelection(selectedDate),
firstDate: firstDate,
lastDate: lastDate,
initiallyShownDate: initiallyShowDate,
onChanged: onChanged,
onSelectionError: onSelectionError,
datePickerLayoutSettings: datePickerLayoutSettings,
datePickerStyles: datePickerStyles ?? DatePickerRangeStyles(),
datePickerKeys: datePickerKeys,
eventDecorationBuilder: eventDecorationBuilder,
onMonthChanged: onMonthChanged,
);
}
}