Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:flutter/material.dart';
4 : import 'package:flutter_localizations/flutter_localizations.dart';
5 :
6 : import 'package:intl/intl.dart' as intl;
7 :
8 : import 'basic_day_based_widget.dart';
9 : import 'date_picker_keys.dart';
10 : import 'day_based_changeable_picker_presenter.dart';
11 : import 'day_picker_selection.dart';
12 : import 'day_type.dart';
13 : import 'i_selectable_picker.dart';
14 : import 'month_navigation_row.dart';
15 : import 'semantic_sorting.dart';
16 : import 'styles/date_picker_styles.dart';
17 : import 'styles/event_decoration.dart';
18 : import 'styles/layout_settings.dart';
19 : import 'typedefs.dart';
20 : import 'utils.dart';
21 :
22 : const Locale _defaultLocale = Locale('en', 'US');
23 :
24 : /// Date picker based on [DayBasedPicker] picker (for days, weeks, ranges).
25 : /// Allows select previous/next month.
26 : class DayBasedChangeablePicker<T> extends StatefulWidget {
27 : /// The currently selected date.
28 : ///
29 : /// This date is highlighted in the picker.
30 : final DayPickerSelection selection;
31 :
32 : /// Called when the user picks a new T.
33 : final ValueChanged<T> onChanged;
34 :
35 : /// Called when the error was thrown after user selection.
36 : final OnSelectionError? onSelectionError;
37 :
38 : /// The earliest date the user is permitted to pick.
39 : final DateTime firstDate;
40 :
41 : /// The latest date the user is permitted to pick.
42 : final DateTime lastDate;
43 :
44 : /// Date for defining what month should be shown initially.
45 : ///
46 : /// Default value is [selection.earliest] for non empty selection
47 : /// and [DateTime.now()] for empty selection.
48 : final DateTime initiallyShowDate;
49 :
50 : /// Layout settings what can be customized by user
51 : final DatePickerLayoutSettings datePickerLayoutSettings;
52 :
53 : /// Styles what can be customized by user
54 : final DatePickerRangeStyles datePickerStyles;
55 :
56 : /// Some keys useful for integration tests
57 : final DatePickerKeys? datePickerKeys;
58 :
59 : /// Logic for date selections.
60 : final ISelectablePicker<T> selectablePicker;
61 :
62 : /// Builder to get event decoration for each date.
63 : ///
64 : /// All event styles are overridden by selected styles
65 : /// except days with dayType is [DayType.notSelected].
66 : final EventDecorationBuilder? eventDecorationBuilder;
67 :
68 : /// Called when the user changes the month
69 : final ValueChanged<DateTime>? onMonthChanged;
70 :
71 : /// Create picker with option to change month.
72 0 : DayBasedChangeablePicker(
73 : {Key? key,
74 : required this.selection,
75 : required this.onChanged,
76 : required this.firstDate,
77 : required this.lastDate,
78 : required this.datePickerLayoutSettings,
79 : required this.datePickerStyles,
80 : required this.selectablePicker,
81 : DateTime? initiallyShownDate,
82 : this.datePickerKeys,
83 : this.onSelectionError,
84 : this.eventDecorationBuilder,
85 : this.onMonthChanged})
86 : : initiallyShowDate =
87 0 : _getInitiallyShownDate(initiallyShownDate, selection),
88 0 : super(key: key);
89 :
90 0 : @override
91 : State<DayBasedChangeablePicker<T>> createState() =>
92 0 : _DayBasedChangeablePickerState<T>();
93 : }
94 :
95 : // todo: Check initial selection and call onSelectionError in case it has error
96 : // todo: (ISelectablePicker.curSelectionIsCorrupted);
97 : class _DayBasedChangeablePickerState<T>
98 : extends State<DayBasedChangeablePicker<T>> {
99 : DateTime _todayDate = DateTime.now();
100 :
101 : Locale curLocale = _defaultLocale;
102 :
103 : MaterialLocalizations localizations = _defaultLocalizations;
104 :
105 : PageController _dayPickerController = PageController();
106 :
107 : // Styles from widget fulfilled with current Theme.
108 : DatePickerRangeStyles _resultStyles = DatePickerRangeStyles();
109 :
110 : DayBasedChangeablePickerPresenter _presenter = _defaultPresenter;
111 :
112 : Timer? _timer;
113 : StreamSubscription<T>? _changesSubscription;
114 :
115 0 : @override
116 : void initState() {
117 0 : super.initState();
118 :
119 : // Initially display the pre-selected date.
120 0 : final int monthPage = _getInitPage();
121 0 : _dayPickerController = PageController(initialPage: monthPage);
122 :
123 0 : _changesSubscription = widget.selectablePicker.onUpdate
124 0 : .listen((newSelectedDate) => widget.onChanged(newSelectedDate))
125 0 : ..onError((e) => widget.onSelectionError != null
126 0 : ? widget.onSelectionError!.call(e)
127 0 : : print(e.toString()));
128 :
129 0 : _updateCurrentDate();
130 0 : _initPresenter();
131 : }
132 :
133 0 : @override
134 : void didUpdateWidget(DayBasedChangeablePicker<T> oldWidget) {
135 0 : super.didUpdateWidget(oldWidget);
136 :
137 0 : if (widget.datePickerStyles != oldWidget.datePickerStyles) {
138 0 : final ThemeData theme = Theme.of(context);
139 0 : _resultStyles = widget.datePickerStyles.fulfillWithTheme(theme);
140 : }
141 :
142 0 : if (widget.selectablePicker != oldWidget.selectablePicker) {
143 0 : _changesSubscription = widget.selectablePicker.onUpdate
144 0 : .listen((newSelectedDate) => widget.onChanged(newSelectedDate))
145 0 : ..onError((e) => widget.onSelectionError != null
146 0 : ? widget.onSelectionError!.call(e)
147 0 : : print(e.toString()));
148 : }
149 : }
150 :
151 0 : @override
152 : void didChangeDependencies() {
153 0 : super.didChangeDependencies();
154 0 : curLocale = Localizations.localeOf(context);
155 :
156 : MaterialLocalizations? curLocalizations =
157 0 : Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
158 0 : if (curLocalizations != null && localizations != curLocalizations) {
159 0 : localizations = curLocalizations;
160 0 : _initPresenter();
161 : }
162 :
163 0 : final ThemeData theme = Theme.of(context);
164 0 : _resultStyles = widget.datePickerStyles.fulfillWithTheme(theme);
165 : }
166 :
167 0 : @override
168 : // ignore: prefer_expression_function_bodies
169 : Widget build(BuildContext context) {
170 0 : return SizedBox(
171 0 : width: widget.datePickerLayoutSettings.monthPickerPortraitWidth,
172 0 : height: widget.datePickerLayoutSettings.maxDayPickerHeight,
173 0 : child: Column(
174 0 : children: <Widget>[
175 0 : widget.datePickerLayoutSettings.hideMonthNavigationRow
176 : ? const SizedBox()
177 0 : : SizedBox(
178 0 : height: widget.datePickerLayoutSettings.dayPickerRowHeight,
179 0 : child: Padding(
180 : //match _DayPicker main layout padding
181 0 : padding: widget.datePickerLayoutSettings.contentPadding,
182 0 : child: _buildMonthNavigationRow()),
183 : ),
184 0 : Expanded(
185 0 : child: Semantics(
186 : sortKey: MonthPickerSortKey.calendar,
187 0 : child: _buildDayPickerPageView(),
188 : ),
189 : ),
190 : ],
191 : ));
192 : }
193 :
194 0 : @override
195 : void dispose() {
196 0 : _timer?.cancel();
197 0 : _dayPickerController.dispose();
198 0 : _changesSubscription?.cancel();
199 0 : widget.selectablePicker.dispose();
200 0 : _presenter.dispose();
201 0 : super.dispose();
202 : }
203 :
204 0 : void _updateCurrentDate() {
205 0 : _todayDate = DateTime.now();
206 : final DateTime tomorrow =
207 0 : DateTime(_todayDate.year, _todayDate.month, _todayDate.day + 1);
208 0 : Duration timeUntilTomorrow = tomorrow.difference(_todayDate);
209 0 : timeUntilTomorrow +=
210 : const Duration(seconds: 1); // so we don't miss it by rounding
211 0 : _timer?.cancel();
212 0 : _timer = Timer(timeUntilTomorrow, () {
213 0 : setState(_updateCurrentDate);
214 : });
215 : }
216 :
217 : // ignore: prefer_expression_function_bodies
218 0 : Widget _buildMonthNavigationRow() {
219 0 : return StreamBuilder<DayBasedChangeablePickerState>(
220 0 : stream: _presenter.data,
221 0 : initialData: _presenter.lastVal,
222 0 : builder: (context, snapshot) {
223 0 : if (!snapshot.hasData) {
224 : return const SizedBox();
225 : }
226 :
227 0 : DayBasedChangeablePickerState state = snapshot.data!;
228 :
229 0 : return MonthNavigationRow(
230 0 : previousPageIconKey: widget.datePickerKeys?.previousPageIconKey,
231 0 : nextPageIconKey: widget.datePickerKeys?.nextPageIconKey,
232 0 : previousMonthTooltip: state.prevTooltip,
233 0 : nextMonthTooltip: state.nextTooltip,
234 : onPreviousMonthTapped:
235 0 : state.isFirstMonth ? null : _presenter.gotoPrevMonth,
236 : onNextMonthTapped:
237 0 : state.isLastMonth ? null : _presenter.gotoNextMonth,
238 0 : title: Text(
239 0 : state.curMonthDis,
240 0 : key: widget.datePickerKeys?.selectedPeriodKeys,
241 0 : style: _resultStyles.displayedPeriodTitle,
242 : ),
243 0 : nextIcon: widget.datePickerStyles.nextIcon,
244 0 : prevIcon: widget.datePickerStyles.prevIcon,
245 : );
246 : });
247 : }
248 :
249 0 : Widget _buildDayPickerPageView() => PageView.builder(
250 0 : controller: _dayPickerController,
251 : scrollDirection: Axis.horizontal,
252 : itemCount:
253 0 : DatePickerUtils.monthDelta(widget.firstDate, widget.lastDate) + 1,
254 0 : itemBuilder: _buildCalendar,
255 0 : onPageChanged: _handleMonthPageChanged,
256 : );
257 :
258 0 : Widget _buildCalendar(BuildContext context, int index) {
259 : final DateTime targetDate =
260 0 : DatePickerUtils.addMonthsToMonthDate(widget.firstDate, index);
261 :
262 0 : return DayBasedPicker(
263 0 : key: ValueKey<DateTime>(targetDate),
264 0 : selectablePicker: widget.selectablePicker,
265 0 : currentDate: _todayDate,
266 0 : firstDate: widget.firstDate,
267 0 : lastDate: widget.lastDate,
268 : displayedMonth: targetDate,
269 0 : datePickerLayoutSettings: widget.datePickerLayoutSettings,
270 0 : selectedPeriodKey: widget.datePickerKeys?.selectedPeriodKeys,
271 0 : datePickerStyles: _resultStyles,
272 0 : eventDecorationBuilder: widget.eventDecorationBuilder,
273 0 : localizations: localizations,
274 : );
275 : }
276 :
277 : // Returns appropriate date to be shown for init.
278 : // If [widget.initiallyShowDate] is out of bounds [widget.firstDate]
279 : // - [widget.lastDate], nearest bound will be used.
280 0 : DateTime _getCheckedInitialDate() {
281 0 : DateTime initiallyShowDateChecked = widget.initiallyShowDate;
282 0 : if (initiallyShowDateChecked.isBefore(widget.firstDate)) {
283 0 : initiallyShowDateChecked = widget.firstDate;
284 : }
285 :
286 0 : if (initiallyShowDateChecked.isAfter(widget.lastDate)) {
287 0 : initiallyShowDateChecked = widget.lastDate;
288 : }
289 :
290 : return initiallyShowDateChecked;
291 : }
292 :
293 0 : int _getInitPage() {
294 0 : final initialDate = _getCheckedInitialDate();
295 0 : int initPage = DatePickerUtils.monthDelta(widget.firstDate, initialDate);
296 :
297 : return initPage;
298 : }
299 :
300 0 : void _initPresenter() {
301 0 : _presenter.dispose();
302 :
303 0 : _presenter = DayBasedChangeablePickerPresenter(
304 0 : firstDate: widget.firstDate,
305 0 : lastDate: widget.lastDate,
306 0 : localizations: localizations,
307 0 : showPrevMonthDates: widget.datePickerLayoutSettings.showPrevMonthEnd,
308 0 : showNextMonthDates: widget.datePickerLayoutSettings.showNextMonthStart,
309 0 : firstDayOfWeekIndex: widget.datePickerStyles.firstDayOfeWeekIndex);
310 0 : _presenter.data.listen(_onStateChanged);
311 :
312 : // date used to define what month should be shown
313 0 : DateTime initSelection = _getCheckedInitialDate();
314 :
315 : // Give information about initial selection to presenter.
316 : // It should be done after first frame when PageView is already created.
317 : // Otherwise event from presenter will cause a error.
318 0 : WidgetsBinding.instance!.addPostFrameCallback((_) {
319 0 : _presenter.setSelectedDate(initSelection);
320 : });
321 : }
322 :
323 0 : void _onStateChanged(DayBasedChangeablePickerState newState) {
324 0 : DateTime newMonth = newState.currentMonth;
325 : final int monthPage =
326 0 : DatePickerUtils.monthDelta(widget.firstDate, newMonth);
327 0 : _dayPickerController.animateToPage(monthPage,
328 : duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
329 : }
330 :
331 0 : void _handleMonthPageChanged(int monthPage) {
332 0 : DateTime firstMonth = widget.firstDate;
333 0 : DateTime newMonth = DateTime(firstMonth.year, firstMonth.month + monthPage);
334 0 : _presenter.changeMonth(newMonth);
335 :
336 0 : widget.onMonthChanged?.call(newMonth);
337 : }
338 :
339 0 : static MaterialLocalizations get _defaultLocalizations =>
340 0 : MaterialLocalizationEn(
341 : twoDigitZeroPaddedFormat:
342 0 : intl.NumberFormat('00', _defaultLocale.toString()),
343 0 : fullYearFormat: intl.DateFormat.y(_defaultLocale.toString()),
344 0 : longDateFormat: intl.DateFormat.yMMMMEEEEd(_defaultLocale.toString()),
345 0 : shortMonthDayFormat: intl.DateFormat.MMMd(_defaultLocale.toString()),
346 : decimalFormat:
347 0 : intl.NumberFormat.decimalPattern(_defaultLocale.toString()),
348 0 : shortDateFormat: intl.DateFormat.yMMMd(_defaultLocale.toString()),
349 0 : mediumDateFormat: intl.DateFormat.MMMEd(_defaultLocale.toString()),
350 0 : compactDateFormat: intl.DateFormat.yMd(_defaultLocale.toString()),
351 0 : yearMonthFormat: intl.DateFormat.yMMMM(_defaultLocale.toString()),
352 : );
353 :
354 0 : static DayBasedChangeablePickerPresenter get _defaultPresenter =>
355 0 : DayBasedChangeablePickerPresenter(
356 0 : firstDate: DateTime.now(),
357 0 : lastDate: DateTime.now(),
358 0 : localizations: _defaultLocalizations,
359 : showPrevMonthDates: false,
360 : showNextMonthDates: false,
361 : firstDayOfWeekIndex: 1);
362 : }
363 :
364 0 : DateTime _getInitiallyShownDate(
365 : DateTime? initiallyShowDate,
366 : DayPickerSelection selection,
367 : ) {
368 : if (initiallyShowDate != null) return initiallyShowDate;
369 0 : if (selection.isNotEmpty) return selection.earliest;
370 0 : return DateTime.now();
371 : }
|