1
0
mirror of https://github.com/nisrulz/flutter-examples.git synced 2025-11-09 13:09:03 +00:00
Files
flutter-examples/flutter_date_pickers-master/lib/src/day_based_changable_picker.dart
2022-10-22 20:29:43 +02:00

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);
}