mirror of
https://github.com/nisrulz/flutter-examples.git
synced 2025-11-09 13:09:03 +00:00
364 lines
12 KiB
Dart
364 lines
12 KiB
Dart
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);
|
|
}
|