Line data Source code
1 : import 'package:flutter/material.dart';
2 : import 'package:intl/intl.dart' as intl hide Locale;
3 :
4 : import 'date_picker_mixin.dart';
5 : import 'day_type.dart';
6 : import 'i_selectable_picker.dart';
7 : import 'styles/date_picker_styles.dart';
8 : import 'styles/event_decoration.dart';
9 : import 'styles/layout_settings.dart';
10 : import 'utils.dart';
11 :
12 : /// Widget for date pickers based on days and cover entire month.
13 : /// Each cell of this picker is day.
14 : class DayBasedPicker<T> extends StatelessWidget with CommonDatePickerFunctions {
15 : /// Selection logic.
16 : final ISelectablePicker selectablePicker;
17 :
18 : /// The current date at the time the picker is displayed.
19 : final DateTime currentDate;
20 :
21 : /// The earliest date the user is permitted to pick.
22 : /// (only year, month and day matter, time doesn't matter)
23 : final DateTime firstDate;
24 :
25 : /// The latest date the user is permitted to pick.
26 : /// (only year, month and day matter, time doesn't matter)
27 : final DateTime lastDate;
28 :
29 : /// The month whose days are displayed by this picker.
30 : final DateTime displayedMonth;
31 :
32 : /// Layout settings what can be customized by user
33 : final DatePickerLayoutSettings datePickerLayoutSettings;
34 :
35 : /// Key fo selected month (useful for integration tests)
36 : final Key? selectedPeriodKey;
37 :
38 : /// Styles what can be customized by user
39 : final DatePickerRangeStyles datePickerStyles;
40 :
41 : /// Builder to get event decoration for each date.
42 : ///
43 : /// All event styles are overridden by selected styles
44 : /// except days with dayType is [DayType.notSelected].
45 : final EventDecorationBuilder? eventDecorationBuilder;
46 :
47 : /// Localizations used to get strings for prev/next button tooltips,
48 : /// weekday headers and display values for days numbers.
49 : ///
50 : // ignore: comment_references
51 : /// If day headers builder is provided [datePickerStyles.dayHeaderBuilder]
52 : /// it will be used for building weekday headers instead of localizations.
53 : final MaterialLocalizations localizations;
54 :
55 : /// Creates main date picker view where every cell is day.
56 0 : DayBasedPicker(
57 : {Key? key,
58 : required this.currentDate,
59 : required this.firstDate,
60 : required this.lastDate,
61 : required this.displayedMonth,
62 : required this.datePickerLayoutSettings,
63 : required this.datePickerStyles,
64 : required this.selectablePicker,
65 : required this.localizations,
66 : this.selectedPeriodKey,
67 : this.eventDecorationBuilder})
68 0 : : assert(!firstDate.isAfter(lastDate)),
69 0 : super(key: key);
70 :
71 0 : @override
72 : Widget build(BuildContext context) {
73 0 : final List<Widget> labels = <Widget>[];
74 :
75 0 : List<Widget> headers = _buildHeaders(localizations, context);
76 0 : List<Widget> daysBeforeMonthStart = _buildCellsBeforeStart(localizations);
77 0 : List<Widget> monthDays = _buildMonthCells(localizations);
78 0 : List<Widget> daysAfterMonthEnd = _buildCellsAfterEnd(localizations);
79 :
80 0 : labels.addAll(headers);
81 0 : labels.addAll(daysBeforeMonthStart);
82 0 : labels.addAll(monthDays);
83 0 : labels.addAll(daysAfterMonthEnd);
84 :
85 0 : return Padding(
86 0 : padding: datePickerLayoutSettings.contentPadding,
87 0 : child: Column(
88 0 : children: <Widget>[
89 0 : Flexible(
90 0 : child: GridView.custom(
91 0 : physics: datePickerLayoutSettings.scrollPhysics,
92 0 : gridDelegate: datePickerLayoutSettings.dayPickerGridDelegate,
93 : childrenDelegate:
94 0 : SliverChildListDelegate(labels, addRepaintBoundaries: false),
95 : ),
96 : ),
97 : ],
98 : ),
99 : );
100 : }
101 :
102 0 : List<Widget> _buildHeaders(
103 : MaterialLocalizations localizations,
104 : BuildContext context,
105 : ) {
106 0 : final int firstDayOfWeekIndex = datePickerStyles.firstDayOfeWeekIndex ??
107 0 : localizations.firstDayOfWeekIndex;
108 :
109 : DayHeaderStyleBuilder dayHeaderStyleBuilder =
110 0 : datePickerStyles.dayHeaderStyleBuilder ??
111 : // ignore: avoid_types_on_closure_parameters
112 0 : (int i) => datePickerStyles.dayHeaderStyle;
113 :
114 0 : final weekdayTitles = _getWeekdayTitles(context);
115 0 : List<Widget> headers = getDayHeaders(
116 : dayHeaderStyleBuilder,
117 : weekdayTitles,
118 : firstDayOfWeekIndex,
119 : );
120 :
121 : return headers;
122 : }
123 :
124 0 : List<String> _getWeekdayTitles(BuildContext context) {
125 0 : final curLocale = Localizations.maybeLocaleOf(context) ?? _defaultLocale;
126 :
127 : // There is no access to weekdays full titles from [MaterialLocalizations]
128 : // so use intl to get it.
129 : final fullLocalizedWeekdayHeaders =
130 0 : intl.DateFormat.E(curLocale.toLanguageTag()).dateSymbols.WEEKDAYS;
131 :
132 0 : final narrowLocalizedWeekdayHeaders = localizations.narrowWeekdays;
133 :
134 : final weekdayTitles =
135 0 : List.generate(fullLocalizedWeekdayHeaders.length, (dayOfWeek) {
136 0 : final builtHeader = datePickerStyles.dayHeaderTitleBuilder
137 : ?.call(dayOfWeek, fullLocalizedWeekdayHeaders);
138 0 : final result = builtHeader ?? narrowLocalizedWeekdayHeaders[dayOfWeek];
139 :
140 : return result;
141 : });
142 :
143 : return weekdayTitles;
144 : }
145 :
146 0 : List<Widget> _buildCellsBeforeStart(MaterialLocalizations localizations) {
147 0 : List<Widget> result = [];
148 :
149 0 : final int year = displayedMonth.year;
150 0 : final int month = displayedMonth.month;
151 0 : final int firstDayOfWeekIndex = datePickerStyles.firstDayOfeWeekIndex ??
152 0 : localizations.firstDayOfWeekIndex;
153 : final int firstDayOffset =
154 0 : computeFirstDayOffset(year, month, firstDayOfWeekIndex);
155 :
156 0 : final bool showDates = datePickerLayoutSettings.showPrevMonthEnd;
157 : if (showDates) {
158 0 : int prevMonth = month - 1;
159 0 : if (prevMonth < 1) prevMonth = 12;
160 0 : int prevYear = prevMonth == 12 ? year - 1 : year;
161 :
162 0 : int daysInPrevMonth = DatePickerUtils.getDaysInMonth(prevYear, prevMonth);
163 0 : List<Widget> days = List.generate(firstDayOffset, (index) => index)
164 0 : .reversed
165 0 : .map((i) => daysInPrevMonth - i)
166 0 : .map((day) => _buildCell(prevYear, prevMonth, day))
167 0 : .toList();
168 :
169 : result = days;
170 : } else {
171 0 : result = List.generate(firstDayOffset, (_) => const SizedBox.shrink());
172 : }
173 :
174 : return result;
175 : }
176 :
177 0 : List<Widget> _buildMonthCells(MaterialLocalizations localizations) {
178 0 : List<Widget> result = [];
179 :
180 0 : final int year = displayedMonth.year;
181 0 : final int month = displayedMonth.month;
182 0 : final int daysInMonth = DatePickerUtils.getDaysInMonth(year, month);
183 :
184 0 : for (int i = 1; i <= daysInMonth; i += 1) {
185 0 : Widget dayWidget = _buildCell(year, month, i);
186 0 : result.add(dayWidget);
187 : }
188 :
189 : return result;
190 : }
191 :
192 0 : List<Widget> _buildCellsAfterEnd(MaterialLocalizations localizations) {
193 0 : List<Widget> result = [];
194 0 : final bool showDates = datePickerLayoutSettings.showNextMonthStart;
195 : if (!showDates) return result;
196 :
197 0 : final int year = displayedMonth.year;
198 0 : final int month = displayedMonth.month;
199 0 : final int firstDayOfWeekIndex = datePickerStyles.firstDayOfeWeekIndex ??
200 0 : localizations.firstDayOfWeekIndex;
201 : final int firstDayOffset =
202 0 : computeFirstDayOffset(year, month, firstDayOfWeekIndex);
203 0 : final int daysInMonth = DatePickerUtils.getDaysInMonth(year, month);
204 0 : final int totalFilledDays = firstDayOffset + daysInMonth;
205 :
206 0 : int reminder = totalFilledDays % 7;
207 0 : if (reminder == 0) return result;
208 0 : final int emptyCellsNum = 7 - reminder;
209 :
210 0 : int nextMonth = month + 1;
211 0 : result = List.generate(emptyCellsNum, (i) => i + 1)
212 0 : .map((day) => _buildCell(year, nextMonth, day))
213 0 : .toList();
214 :
215 : return result;
216 : }
217 :
218 0 : Widget _buildCell(int year, int month, int day) {
219 0 : DateTime dayToBuild = DateTime(year, month, day);
220 0 : dayToBuild = _checkDateTime(dayToBuild);
221 :
222 0 : DayType dayType = selectablePicker.getDayType(dayToBuild);
223 :
224 0 : Widget dayWidget = _DayCell(
225 : day: dayToBuild,
226 0 : currentDate: currentDate,
227 0 : selectablePicker: selectablePicker,
228 0 : datePickerStyles: datePickerStyles,
229 0 : eventDecorationBuilder: eventDecorationBuilder,
230 0 : localizations: localizations,
231 : );
232 :
233 0 : if (dayType != DayType.disabled) {
234 0 : dayWidget = GestureDetector(
235 : behavior: HitTestBehavior.opaque,
236 0 : onTap: () => selectablePicker.onDayTapped(dayToBuild),
237 : child: dayWidget,
238 : );
239 : }
240 :
241 : return dayWidget;
242 : }
243 :
244 : /// Checks if [DateTime] is same day as [lastDate] or [firstDate]
245 : /// and returns dt corrected (with time of [lastDate] or [firstDate]).
246 0 : DateTime _checkDateTime(DateTime dt) {
247 : DateTime result = dt;
248 :
249 : // If dayToBuild is the first day we need to save original time for it.
250 0 : if (DatePickerUtils.sameDate(dt, firstDate)) result = firstDate;
251 :
252 : // If dayToBuild is the last day we need to save original time for it.
253 0 : if (DatePickerUtils.sameDate(dt, lastDate)) result = lastDate;
254 :
255 : return result;
256 : }
257 : }
258 :
259 : class _DayCell extends StatelessWidget {
260 : /// Day for this cell.
261 : final DateTime day;
262 :
263 : /// Selection logic.
264 : final ISelectablePicker selectablePicker;
265 :
266 : /// Styles what can be customized by user
267 : final DatePickerRangeStyles datePickerStyles;
268 :
269 : /// The current date at the time the picker is displayed.
270 : final DateTime currentDate;
271 :
272 : /// Builder to get event decoration for each date.
273 : ///
274 : /// All event styles are overridden by selected styles
275 : /// except days with dayType is [DayType.notSelected].
276 : final EventDecorationBuilder? eventDecorationBuilder;
277 :
278 : final MaterialLocalizations localizations;
279 :
280 0 : const _DayCell(
281 : {Key? key,
282 : required this.day,
283 : required this.selectablePicker,
284 : required this.datePickerStyles,
285 : required this.currentDate,
286 : required this.localizations,
287 : this.eventDecorationBuilder})
288 0 : : super(key: key);
289 :
290 0 : @override
291 : Widget build(BuildContext context) {
292 0 : DayType dayType = selectablePicker.getDayType(day);
293 :
294 : BoxDecoration? decoration;
295 : TextStyle? itemStyle;
296 :
297 0 : if (dayType != DayType.disabled && dayType != DayType.notSelected) {
298 0 : itemStyle = _getSelectedTextStyle(dayType);
299 0 : decoration = _getSelectedDecoration(dayType);
300 0 : } else if (dayType == DayType.disabled) {
301 0 : itemStyle = datePickerStyles.disabledDateStyle;
302 0 : } else if (DatePickerUtils.sameDate(currentDate, day)) {
303 0 : itemStyle = datePickerStyles.currentDateStyle;
304 : } else {
305 0 : itemStyle = datePickerStyles.defaultDateTextStyle;
306 : }
307 :
308 : // Merges decoration and textStyle with [EventDecoration].
309 : //
310 : // Merges only in cases if [dayType] is DayType.notSelected.
311 : // If day is current day it is also gets event decoration
312 : // instead of decoration for current date.
313 0 : if (dayType == DayType.notSelected && eventDecorationBuilder != null) {
314 0 : EventDecoration? eDecoration = eventDecorationBuilder != null
315 0 : ? eventDecorationBuilder!.call(day)
316 : : null;
317 :
318 0 : decoration = eDecoration?.boxDecoration ?? decoration;
319 0 : itemStyle = eDecoration?.textStyle ?? itemStyle;
320 : }
321 :
322 0 : String semanticLabel = '${localizations.formatDecimal(day.day)}, '
323 0 : '${localizations.formatFullDate(day)}';
324 :
325 : bool daySelected =
326 0 : dayType != DayType.disabled && dayType != DayType.notSelected;
327 :
328 0 : Widget dayWidget = Container(
329 : decoration: decoration,
330 0 : child: Center(
331 0 : child: Semantics(
332 : // We want the day of month to be spoken first irrespective of the
333 : // locale-specific preferences or TextDirection. This is because
334 : // an accessibility user is more likely to be interested in the
335 : // day of month before the rest of the date, as they are looking
336 : // for the day of month. To do that we prepend day of month to the
337 : // formatted full date.
338 : label: semanticLabel,
339 : selected: daySelected,
340 0 : child: ExcludeSemantics(
341 0 : child: Text(localizations.formatDecimal(day.day), style: itemStyle),
342 : ),
343 : ),
344 : ),
345 : );
346 :
347 : return dayWidget;
348 : }
349 :
350 0 : BoxDecoration? _getSelectedDecoration(DayType dayType) {
351 : BoxDecoration? result;
352 :
353 0 : if (dayType == DayType.single) {
354 0 : result = datePickerStyles.selectedSingleDateDecoration;
355 0 : } else if (dayType == DayType.start) {
356 0 : result = datePickerStyles.selectedPeriodStartDecoration;
357 0 : } else if (dayType == DayType.end) {
358 0 : result = datePickerStyles.selectedPeriodLastDecoration;
359 : } else {
360 0 : result = datePickerStyles.selectedPeriodMiddleDecoration;
361 : }
362 :
363 : return result;
364 : }
365 :
366 0 : TextStyle? _getSelectedTextStyle(DayType dayType) {
367 : TextStyle? result;
368 :
369 0 : if (dayType == DayType.single) {
370 0 : result = datePickerStyles.selectedDateStyle;
371 0 : } else if (dayType == DayType.start) {
372 0 : result = datePickerStyles.selectedPeriodStartTextStyle;
373 0 : } else if (dayType == DayType.end) {
374 0 : result = datePickerStyles.selectedPeriodEndTextStyle;
375 : } else {
376 0 : result = datePickerStyles.selectedPeriodMiddleTextStyle;
377 : }
378 :
379 : return result;
380 : }
381 : }
382 :
383 0 : Locale _defaultLocale = const Locale('en', 'US');
|