import 'dart:async'; import 'dart:math' as math; import 'package:eva_icons_flutter/eva_icons_flutter.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'button_actions.dart'; import 'era_mode.dart'; /// Initial display mode of the date picker dialog. /// /// Date picker UI mode for either showing a list of available years or a /// monthly calendar initially in the dialog shown by calling [showDatePicker]. /// const Duration _kMonthScrollDuration = Duration(milliseconds: 200); const double _kDayPickerRowHeight = 42.0; const int _kMaxDayPickerRowCount = 6; // A 31 day month that starts on Saturday. // Two extra rows: one for the day-of-week header and one for the month header. const double _kMaxDayPickerHeight = _kDayPickerRowHeight * (_kMaxDayPickerRowCount + 2); /// Shows the selected date in large font and toggles between year and day mode class _DatePickerHeader extends StatelessWidget { const _DatePickerHeader({ Key key, @required this.selectedDate, @required this.mode, @required this.onModeChanged, @required this.orientation, this.yearSelectAllowed, this.era, this.borderRadius, this.imageHeader, this.description = "", this.fontFamily, }) : assert(selectedDate != null), assert(mode != null), assert(orientation != null), super(key: key); final DateTime selectedDate; final DatePickerMode mode; final ValueChanged onModeChanged; final Orientation orientation; final bool yearSelectAllowed; /// Era custom final EraMode era; /// Border final double borderRadius; /// Header final ImageProvider imageHeader; /// Header description final String description; /// Font final String fontFamily; void _handleChangeMode(DatePickerMode value) { if (value != mode) onModeChanged(value); } @override Widget build(BuildContext context) { final MaterialLocalizations localizations = MaterialLocalizations.of(context); final ThemeData themeData = Theme.of(context); final TextTheme headerTextTheme = themeData.primaryTextTheme; Color dayColor; Color yearColor; switch (themeData.primaryColorBrightness) { case Brightness.light: dayColor = mode == DatePickerMode.day ? Colors.black87 : Colors.black54; yearColor = mode == DatePickerMode.year ? Colors.black87 : Colors.black54; break; case Brightness.dark: dayColor = mode == DatePickerMode.day ? Colors.white : Colors.white70; yearColor = mode == DatePickerMode.year ? Colors.white : Colors.white70; break; } final TextStyle dayStyle = headerTextTheme.headline4.copyWith( color: dayColor, fontFamily: fontFamily, fontWeight: FontWeight.w800); final TextStyle yearStyle = headerTextTheme.subtitle1.copyWith( color: yearColor, fontFamily: fontFamily, fontWeight: FontWeight.w800); Color backgroundColor; backgroundColor = themeData.primaryColor; EdgeInsets padding; MainAxisAlignment mainAxisAlignment; switch (orientation) { case Orientation.portrait: padding = const EdgeInsets.all(16.0); mainAxisAlignment = MainAxisAlignment.center; break; case Orientation.landscape: padding = const EdgeInsets.all(8.0); mainAxisAlignment = MainAxisAlignment.start; break; } final Widget yearButton = IgnorePointer( ignoring: mode != DatePickerMode.day, ignoringSemantics: false, child: IgnorePointer( ignoring: !yearSelectAllowed ?? true, child: _DateHeaderButton( color: Colors.transparent, onTap: Feedback.wrapForTap( () => _handleChangeMode(DatePickerMode.year), context, ), child: Semantics( selected: mode == DatePickerMode.year, child: Text( "${calculateYearEra(era, selectedDate.year)}", style: yearStyle, ), ), ), ), ); final Widget dayButton = IgnorePointer( ignoring: mode == DatePickerMode.day, ignoringSemantics: false, child: _DateHeaderButton( color: Colors.transparent, onTap: Feedback.wrapForTap( () => _handleChangeMode(DatePickerMode.day), context, ), child: Semantics( selected: mode == DatePickerMode.day, child: Text( localizations.formatMediumDate(selectedDate), style: dayStyle, ), ), ), ); BorderRadius borderRadiusData = BorderRadius.only( topLeft: Radius.circular(borderRadius), topRight: Radius.circular(borderRadius), ); if (orientation == Orientation.landscape) { borderRadiusData = BorderRadius.only( topLeft: Radius.circular(borderRadius), bottomLeft: Radius.circular(borderRadius), ); } return Container( decoration: BoxDecoration( image: imageHeader != null ? DecorationImage(image: imageHeader, fit: BoxFit.cover) : null, color: backgroundColor, borderRadius: borderRadiusData, ), padding: padding, child: Column( mainAxisAlignment: mainAxisAlignment, crossAxisAlignment: CrossAxisAlignment.start, children: [ yearButton, dayButton, const SizedBox(height: 4.0), Visibility( visible: description.isNotEmpty, child: Padding( padding: EdgeInsets.symmetric(horizontal: 12), child: Text( description, style: TextStyle( color: yearColor, fontSize: 12, fontFamily: fontFamily, ), ), ), ) ], ), ); } } class _DateHeaderButton extends StatelessWidget { const _DateHeaderButton({ Key key, this.onTap, this.color, this.child, }) : super(key: key); final VoidCallback onTap; final Color color; final Widget child; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return Material( type: MaterialType.button, color: color, child: InkWell( borderRadius: kMaterialEdges[MaterialType.button], highlightColor: theme.highlightColor, splashColor: theme.splashColor, onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: child, ), ), ); } } class _DayPickerGridDelegate extends SliverGridDelegate { const _DayPickerGridDelegate(); @override SliverGridLayout getLayout(SliverConstraints constraints) { const int columnCount = DateTime.daysPerWeek; final double tileWidth = constraints.crossAxisExtent / columnCount; final double viewTileHeight = constraints.viewportMainAxisExtent / (_kMaxDayPickerRowCount + 1); final double tileHeight = math.max(_kDayPickerRowHeight, viewTileHeight); return SliverGridRegularTileLayout( crossAxisCount: columnCount, mainAxisStride: tileHeight, crossAxisStride: tileWidth, childMainAxisExtent: tileHeight, childCrossAxisExtent: tileWidth, reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), ); } @override bool shouldRelayout(_DayPickerGridDelegate oldDelegate) => false; } const _DayPickerGridDelegate _kDayPickerGridDelegate = _DayPickerGridDelegate(); /// Displays the days of a given month and allows choosing a day. /// /// The days are arranged in a rectangular grid with one column for each day of /// the week. /// /// The day picker widget is rarely used directly. Instead, consider using /// [showDatePicker], which creates a date picker dialog. /// /// See also: /// /// * [showDatePicker], which shows a dialog that contains a material design /// date picker. /// * [showTimePicker], which shows a dialog that contains a material design /// time picker. class DayPicker extends StatelessWidget { /// Creates a day picker. /// /// Rarely used directly. Instead, typically used as part of a [MonthPicker]. DayPicker({ Key key, @required this.selectedDate, @required this.currentDate, @required this.onChanged, @required this.firstDate, @required this.lastDate, @required this.displayedMonth, this.selectableDayPredicate, this.dragStartBehavior = DragStartBehavior.start, this.era, this.locale, this.fontFamily, }) : assert(selectedDate != null), assert(currentDate != null), assert(onChanged != null), assert(displayedMonth != null), assert(dragStartBehavior != null), assert(!firstDate.isAfter(lastDate)), assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate)), super(key: key); /// 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; /// Called when the user picks a day. final ValueChanged 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; /// The month whose days are displayed by this picker. final DateTime displayedMonth; /// Optional user supplied predicate function to customize selectable days. final SelectableDayPredicate selectableDayPredicate; final EraMode era; final Locale locale; final String fontFamily; /// Determines the way that drag start behavior is handled. /// /// If set to [DragStartBehavior.start], the drag gesture used to scroll a /// date picker wheel will begin upon the detection of a drag gesture. If set /// to [DragStartBehavior.down] it will begin when a down event is first /// detected. /// /// In general, setting this to [DragStartBehavior.start] will make drag /// animation smoother and setting it to [DragStartBehavior.down] will make /// drag behavior feel slightly more reactive. /// /// By default, the drag start behavior is [DragStartBehavior.start]. /// /// See also: /// /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. final DragStartBehavior dragStartBehavior; /// 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 _getDayHeaders( TextStyle headerStyle, MaterialLocalizations localizations, ) { final List result = []; for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) { final String weekday = localizations.narrowWeekdays[i]; result.add(ExcludeSemantics( child: Center( child: Text(weekday, style: TextStyle( fontWeight: FontWeight.w800, fontFamily: "HKGrotesk", color: Color.fromRGBO(78, 62, 253, 1.0)))), )); if (i == (localizations.firstDayOfWeekIndex - 1) % 7) break; } return result; } // Do not use this directly - call getDaysInMonth instead. static const List _daysInMonth = [ 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); if (isLeapYear) return 29; return 28; } return _daysInMonth[month - 1]; } /// 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, MaterialLocalizations localizations) { // 0-based day of week, with 0 representing Monday. final int weekdayFromMonday = DateTime(year, month).weekday - 1; // 0-based day of week, with 0 representing Sunday. final int firstDayOfWeekFromSunday = localizations.firstDayOfWeekIndex; // 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; } @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context); final int year = displayedMonth.year; final int month = displayedMonth.month; final int daysInMonth = getDaysInMonth(year, month); final int firstDayOffset = _computeFirstDayOffset( year, month, localizations, ); final List labels = _getDayHeaders( themeData.textTheme.caption.copyWith(color: themeData.primaryColor), localizations); for (int i = 0; true; i += 1) { // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on // a leap year. final int day = i - firstDayOffset + 1; if (day > daysInMonth) break; if (day < 1) { labels.add(Container()); } else { final DateTime dayToBuild = DateTime(year, month, day); final bool disabled = dayToBuild.isAfter(lastDate) || dayToBuild.isBefore(firstDate) || (selectableDayPredicate != null && !selectableDayPredicate(dayToBuild)); BoxDecoration decoration; TextStyle itemStyle = themeData.textTheme.bodyText2 .copyWith(fontFamily: fontFamily, fontWeight: FontWeight.w800); final bool isSelectedDay = selectedDate.year == year && selectedDate.month == month && selectedDate.day == day; if (isSelectedDay) { // The selected day gets a circle background highlight, and a contrasting text color. itemStyle = themeData.accentTextTheme.bodyText1.copyWith( fontFamily: fontFamily, fontWeight: FontWeight.w800, color: Colors.white); decoration = BoxDecoration( color: themeData.primaryColor, shape: BoxShape.circle, ); } else if (disabled) { itemStyle = themeData.textTheme.bodyText2.copyWith( color: themeData.disabledColor, fontFamily: fontFamily, fontWeight: FontWeight.w800, ); } else if (currentDate.year == year && currentDate.month == month && currentDate.day == day) { // The current day gets a different text color. itemStyle = themeData.textTheme.bodyText1.copyWith( color: themeData.accentColor, fontFamily: fontFamily, fontWeight: FontWeight.w800); } 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: '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}', selected: isSelectedDay, sortKey: OrdinalSortKey(day.toDouble()), child: ExcludeSemantics( child: Text(localizations.formatDecimal(day), style: itemStyle), ), ), ), ); if (!disabled) { dayWidget = GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { onChanged(dayToBuild); }, child: dayWidget, dragStartBehavior: dragStartBehavior, ); } labels.add(dayWidget); } } String monthYearHeader = ""; monthYearHeader = localizations.formatMonthYear(displayedMonth); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( children: [ Container( height: _kDayPickerRowHeight, child: Center( child: ExcludeSemantics( child: Text( monthYearHeader, style: themeData.textTheme.subtitle1.copyWith( fontFamily: fontFamily, fontWeight: FontWeight.w800), ), ), ), ), Flexible( child: GridView.custom( gridDelegate: _kDayPickerGridDelegate, childrenDelegate: SliverChildListDelegate( labels, addRepaintBoundaries: false, ), padding: EdgeInsets.zero, ), ), ], ), ); } } /// A scrollable list of months to allow picking a month. /// /// Shows the days of each month in a rectangular grid with one column for each /// day of the week. /// /// The month picker widget is rarely used directly. Instead, consider using /// [showDatePicker], which creates a date picker dialog. /// /// See also: /// /// * [showDatePicker], which shows a dialog that contains a material design /// date picker. /// * [showTimePicker], which shows a dialog that contains a material design /// time picker. class MonthPicker extends StatefulWidget { /// Creates a month picker. /// /// Rarely used directly. Instead, typically used as part of the dialog shown /// by [showDatePicker]. MonthPicker({ Key key, @required this.selectedDate, @required this.onChanged, @required this.firstDate, @required this.lastDate, this.selectableDayPredicate, this.dragStartBehavior = DragStartBehavior.start, this.era, this.locale, this.fontFamily, }) : assert(selectedDate != null), assert(onChanged != null), assert(!firstDate.isAfter(lastDate)), assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate)), 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 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; /// Optional user supplied predicate function to customize selectable days. final SelectableDayPredicate selectableDayPredicate; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// Optional era year. final EraMode era; final Locale locale; /// Font final String fontFamily; @override _MonthPickerState createState() => _MonthPickerState(); } class _MonthPickerState extends State with SingleTickerProviderStateMixin { static final Animatable _chevronOpacityTween = Tween(begin: 1.0, end: 0.0) .chain(CurveTween(curve: Curves.easeInOut)); @override void initState() { super.initState(); // Initially display the pre-selected date. final int monthPage = _monthDelta(widget.firstDate, widget.selectedDate); _dayPickerController = PageController(initialPage: monthPage); _handleMonthPageChanged(monthPage); _updateCurrentDate(); // Setup the fade animation for chevrons _chevronOpacityController = AnimationController( duration: const Duration(milliseconds: 250), vsync: this, ); _chevronOpacityAnimation = _chevronOpacityController.drive( _chevronOpacityTween, ); } @override void didUpdateWidget(MonthPicker oldWidget) { super.didUpdateWidget(oldWidget); if (widget.selectedDate != oldWidget.selectedDate) { final int monthPage = _monthDelta(widget.firstDate, widget.selectedDate); _dayPickerController = PageController(initialPage: monthPage); _handleMonthPageChanged(monthPage); } } MaterialLocalizations localizations; TextDirection textDirection; @override void didChangeDependencies() { super.didChangeDependencies(); localizations = MaterialLocalizations.of(context); textDirection = Directionality.of(context); } DateTime _todayDate; DateTime _currentDisplayedMonthDate; Timer _timer; PageController _dayPickerController; AnimationController _chevronOpacityController; Animation _chevronOpacityAnimation; void _updateCurrentDate() { _todayDate = DateTime.now(); final DateTime tomorrow = DateTime(_todayDate.year, _todayDate.month, _todayDate.day + 1); Duration timeUntilTomorrow = tomorrow.difference(_todayDate); // so we don't miss it by rounding timeUntilTomorrow += const Duration(seconds: 1); _timer?.cancel(); _timer = Timer(timeUntilTomorrow, () { setState(() => _updateCurrentDate()); }); } static int _monthDelta(DateTime startDate, DateTime endDate) { return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month; } /// Add months to a month truncated date. DateTime _addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) { return DateTime( monthDate.year + monthsToAdd ~/ 12, monthDate.month + monthsToAdd % 12, ); } Widget _buildItems(BuildContext context, int index) { final DateTime month = _addMonthsToMonthDate(widget.firstDate, index); return DayPicker( key: ValueKey(month), selectedDate: widget.selectedDate, currentDate: _todayDate, onChanged: widget.onChanged, firstDate: widget.firstDate, lastDate: widget.lastDate, displayedMonth: month, selectableDayPredicate: widget.selectableDayPredicate, dragStartBehavior: widget.dragStartBehavior, era: widget.era, locale: widget.locale, fontFamily: widget.fontFamily, ); } void _handleNextMonth() { if (!_isDisplayingLastMonth) { SemanticsService.announce( localizations.formatMonthYear(_nextMonthDate), textDirection, ); _dayPickerController.nextPage( duration: _kMonthScrollDuration, curve: Curves.ease, ); } } void _handlePreviousMonth() { if (!_isDisplayingFirstMonth) { SemanticsService.announce( localizations.formatMonthYear(_previousMonthDate), textDirection, ); _dayPickerController.previousPage( duration: _kMonthScrollDuration, curve: Curves.ease, ); } } /// True if the earliest allowable month is displayed. bool get _isDisplayingFirstMonth { return !_currentDisplayedMonthDate.isAfter( DateTime(widget.firstDate.year, widget.firstDate.month), ); } /// True if the latest allowable month is displayed. bool get _isDisplayingLastMonth { return !_currentDisplayedMonthDate.isBefore( DateTime(widget.lastDate.year, widget.lastDate.month), ); } DateTime _previousMonthDate; DateTime _nextMonthDate; void _handleMonthPageChanged(int monthPage) { setState(() { _previousMonthDate = _addMonthsToMonthDate( widget.firstDate, monthPage - 1, ); _currentDisplayedMonthDate = _addMonthsToMonthDate( widget.firstDate, monthPage, ); _nextMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage + 1); }); } @override Widget build(BuildContext context) { return SizedBox( // The month picker just adds month navigation to the day picker, so make // it the same height as the DayPicker height: _kMaxDayPickerHeight, child: Stack( children: [ Semantics( sortKey: _MonthPickerSortKey.calendar, child: NotificationListener( onNotification: (_) { _chevronOpacityController.forward(); return false; }, child: NotificationListener( onNotification: (_) { _chevronOpacityController.reverse(); return false; }, child: PageView.builder( physics: BouncingScrollPhysics(), dragStartBehavior: widget.dragStartBehavior, key: ValueKey(widget.selectedDate), controller: _dayPickerController, scrollDirection: Axis.horizontal, itemCount: _monthDelta(widget.firstDate, widget.lastDate) + 1, itemBuilder: _buildItems, onPageChanged: _handleMonthPageChanged, ), ), ), ), /// Arrow Left PositionedDirectional( top: 0.0, start: 8.0, child: Semantics( sortKey: _MonthPickerSortKey.previousMonth, child: FadeTransition( opacity: _chevronOpacityAnimation, child: IconButton( disabledColor: Theme.of(context).disabledColor.withOpacity(0.2), color: Theme.of(context).disabledColor, icon: const Icon(EvaIcons.chevronLeft), tooltip: _isDisplayingFirstMonth ? null : '${localizations.previousMonthTooltip} ${localizations.formatMonthYear(_previousMonthDate)}', onPressed: _isDisplayingFirstMonth == true ? null : _handlePreviousMonth, ), ), ), ), /// Arrow Right PositionedDirectional( top: 0.0, end: 8.0, child: Semantics( sortKey: _MonthPickerSortKey.nextMonth, child: FadeTransition( opacity: _chevronOpacityAnimation, child: IconButton( disabledColor: Theme.of(context).disabledColor.withOpacity(0.2), color: Theme.of(context).disabledColor, icon: const Icon(EvaIcons.chevronRight), tooltip: _isDisplayingLastMonth ? null : '${localizations.nextMonthTooltip} ${localizations.formatMonthYear(_nextMonthDate)}', onPressed: _isDisplayingLastMonth ? null : _handleNextMonth, ), ), ), ), ], ), ); } @override void dispose() { _timer?.cancel(); _chevronOpacityController?.dispose(); _dayPickerController?.dispose(); super.dispose(); } } // Defines semantic traversal order of the top-level widgets inside the month // picker. class _MonthPickerSortKey extends OrdinalSortKey { const _MonthPickerSortKey(double order) : super(order); static const _MonthPickerSortKey previousMonth = _MonthPickerSortKey(1.0); static const _MonthPickerSortKey nextMonth = _MonthPickerSortKey(2.0); static const _MonthPickerSortKey calendar = _MonthPickerSortKey(3.0); } /// A scrollable list of years to allow picking a year. /// /// The year picker widget is rarely used directly. Instead, consider using /// [showDatePicker], which creates a date picker dialog. /// /// Requires one of its ancestors to be a [Material] widget. /// /// See also: /// /// * [showDatePicker], which shows a dialog that contains a material design /// date picker. /// * [showTimePicker], which shows a dialog that contains a material design /// time picker. class YearPicker extends StatefulWidget { /// Creates a year picker. /// /// The [selectedDate] and [onChanged] arguments must not be null. The /// [lastDate] must be after the [firstDate]. /// /// Rarely used directly. Instead, typically used as part of the dialog shown /// by [showDatePicker]. YearPicker({ Key key, @required this.selectedDate, @required this.onChanged, @required this.firstDate, @required this.lastDate, this.era = EraMode.CHRIST_YEAR, this.fontFamily, this.dragStartBehavior = DragStartBehavior.start, }) : assert(selectedDate != null), assert(onChanged != null), assert(!firstDate.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 year. final ValueChanged 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; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// Era final EraMode era; /// Font final String fontFamily; @override _YearPickerState createState() => _YearPickerState(); } class _YearPickerState extends State { static const double _itemExtent = 50.0; ScrollController scrollController; @override void initState() { super.initState(); scrollController = ScrollController( // Move the initial scroll position to the currently selected date's year. initialScrollOffset: (widget.selectedDate.year - widget.firstDate.year) * _itemExtent, ); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final ThemeData themeData = Theme.of(context); final TextStyle style = themeData.textTheme.bodyText2 .copyWith(fontFamily: widget.fontFamily, fontWeight: FontWeight.w700); return ConstrainedBox( constraints: BoxConstraints(maxHeight: 300), child: ListView.builder( physics: BouncingScrollPhysics(), dragStartBehavior: widget.dragStartBehavior, controller: scrollController, itemExtent: _itemExtent, itemCount: widget.lastDate.year - widget.firstDate.year + 1, itemBuilder: (BuildContext context, int index) { final int year = widget.firstDate.year + index; final bool isSelected = year == widget.selectedDate.year; final TextStyle itemStyle = isSelected ? themeData.textTheme.headline5.copyWith( color: themeData.accentColor, fontFamily: widget.fontFamily, fontWeight: FontWeight.w900) : style; return InkWell( key: ValueKey(year), onTap: () { widget.onChanged(DateTime( year, widget.selectedDate.month, widget.selectedDate.day, )); }, child: Center( child: Semantics( selected: isSelected, child: Text( "${calculateYearEra(widget.era, year)}", style: itemStyle, ), ), ), ); }, ), ); } } class DatePickerDialog extends StatefulWidget { const DatePickerDialog({ Key key, this.initialDate, this.yearSelectAllowed, this.firstDate, this.lastDate, this.selectableDayPredicate, this.initialDatePickerMode, this.era, this.locale, this.borderRadius, this.imageHeader, this.description = "", this.fontFamily, this.negativeBtn, this.positiveBtn, this.leftBtn, this.onLeftBtn, }) : super(key: key); final DateTime initialDate; final DateTime firstDate; final DateTime lastDate; final SelectableDayPredicate selectableDayPredicate; final DatePickerMode initialDatePickerMode; final bool yearSelectAllowed; /// Custom era year. final EraMode era; final Locale locale; /// Border final double borderRadius; /// Header; final ImageProvider imageHeader; final String description; /// Font final String fontFamily; final String negativeBtn; final String positiveBtn; final String leftBtn; final VoidCallback onLeftBtn; @override _DatePickerDialogState createState() => _DatePickerDialogState(); } class _DatePickerDialogState extends State { @override void initState() { super.initState(); _selectedDate = widget.initialDate; _mode = widget.initialDatePickerMode; } bool _announcedInitialDate = false; MaterialLocalizations localizations; TextDirection textDirection; @override void didChangeDependencies() { super.didChangeDependencies(); localizations = MaterialLocalizations.of(context); textDirection = Directionality.of(context); if (!_announcedInitialDate) { _announcedInitialDate = true; SemanticsService.announce( localizations.formatFullDate(_selectedDate), textDirection, ); } } DateTime _selectedDate; DatePickerMode _mode; final GlobalKey _pickerKey = GlobalKey(); void _vibrate() { HapticFeedback.vibrate(); } void _handleModeChanged(DatePickerMode mode) { _vibrate(); setState(() { _mode = mode; if (_mode == DatePickerMode.day) { SemanticsService.announce( localizations.formatMonthYear(_selectedDate), textDirection, ); } else { SemanticsService.announce( localizations.formatYear(_selectedDate), textDirection, ); } }); } void _handleYearChanged(DateTime value) { if (value.isBefore(widget.firstDate)) { value = widget.firstDate; } else if (value.isAfter(widget.lastDate)) { value = widget.lastDate; } if (value == _selectedDate) return; _vibrate(); setState(() { _mode = DatePickerMode.day; _selectedDate = value; }); } void _handleDayChanged(DateTime value) { _vibrate(); setState(() { _selectedDate = value; }); } void _handleOk() { Navigator.of(context).pop(_selectedDate); } Widget _buildPicker() { assert(_mode != null); switch (_mode) { case DatePickerMode.day: return MonthPicker( key: _pickerKey, selectedDate: _selectedDate, onChanged: _handleDayChanged, firstDate: widget.firstDate, lastDate: widget.lastDate, era: widget.era, locale: widget.locale, selectableDayPredicate: widget.selectableDayPredicate, fontFamily: widget.fontFamily, ); case DatePickerMode.year: return YearPicker( key: _pickerKey, selectedDate: _selectedDate, onChanged: _handleYearChanged, firstDate: widget.firstDate, lastDate: widget.lastDate, era: widget.era, fontFamily: widget.fontFamily, ); } return null; } @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final Widget picker = _buildPicker(); final Widget actions = ButtonActions(onPositive: _handleOk); final Dialog dialog = Dialog( child: OrientationBuilder( builder: (BuildContext context, Orientation orientation) { assert(orientation != null); final Widget header = _DatePickerHeader( selectedDate: _selectedDate, yearSelectAllowed: widget.yearSelectAllowed ?? true, mode: _mode, onModeChanged: _handleModeChanged, orientation: orientation, era: widget.era, borderRadius: widget.borderRadius, imageHeader: widget.imageHeader, description: widget.description, fontFamily: widget.fontFamily, ); switch (orientation) { case Orientation.portrait: return Container( decoration: BoxDecoration( color: theme.dialogBackgroundColor, borderRadius: BorderRadius.circular(widget.borderRadius), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ header, Flexible(child: picker), actions, ], ), ); case Orientation.landscape: return Container( decoration: BoxDecoration( color: theme.dialogBackgroundColor, borderRadius: BorderRadius.circular(widget.borderRadius), ), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Flexible(child: header), Flexible( flex: 2, // have the picker take up 2/3 of the dialog width child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Flexible(child: picker), actions, ], ), ), ], ), ); } return null; }), ); return Theme( data: theme.copyWith(dialogBackgroundColor: Colors.transparent), child: dialog, ); } } /// Signature for predicating dates for enabled date selections. /// /// See [showDatePicker]. typedef SelectableDayPredicate = bool Function(DateTime day); /// Shows a dialog containing a material design date picker. /// /// The returned [Future] resolves to the date selected by the user when the /// user closes the dialog. If the user cancels the dialog, null is returned. /// /// An optional [selectableDayPredicate] function can be passed in to customize /// the days to enable for selection. If provided, only the days that /// [selectableDayPredicate] returned true for will be selectable. /// /// An optional [initialDatePickerMode] argument can be used to display the /// date picker initially in the year or month+day picker mode. It defaults /// to month+day, and must not be null. /// /// An optional [locale] argument can be used to set the locale for the date /// picker. It defaults to the ambient locale provided by [Localizations]. /// /// An optional [textDirection] argument can be used to set the text direction /// (RTL or LTR) for the date picker. It defaults to the ambient text direction /// provided by [Directionality]. If both [locale] and [textDirection] are not /// null, [textDirection] overrides the direction chosen for the [locale]. /// /// The [context] argument is passed to [showDialog], the documentation for /// which discusses how it is used. /// /// The [builder] parameter can be used to wrap the dialog widget /// to add inherited widgets like [Theme]. /// /// {@tool sample} /// Show a date picker with the dark theme. /// /// ```dart /// Future selectedDate = showDatePicker( /// context: context, /// initialDate: DateTime.now(), /// firstDate: DateTime(2018), /// lastDate: DateTime(2030), /// builder: (BuildContext context, Widget child) { /// return Theme( /// data: ThemeData.dark(), /// child: child, /// ); /// }, /// ); /// ``` /// {@end-tool} /// /// The [context], [initialDate], [firstDate], and [lastDate] parameters must /// not be null. /// /// See also: /// /// * [showTimePicker], which shows a dialog that contains a material design /// time picker. /// * [DayPicker], which displays the days of a given month and allows /// choosing a day. /// * [MonthPicker], which displays a scrollable list of months to allow /// picking a month. /// * [YearPicker], which displays a scrollable list of years to allow picking /// a year. /// Future showRoundedDatePicker( {@required BuildContext context, DateTime initialDate, DateTime firstDate, DateTime lastDate, SelectableDayPredicate selectableDayPredicate, DatePickerMode initialDatePickerMode = DatePickerMode.day, Locale locale, TextDirection textDirection, ThemeData theme, double borderRadius = 16, EraMode era = EraMode.CHRIST_YEAR, ImageProvider imageHeader, String description = "", String fontFamily, bool barrierDismissible = false, Color background = Colors.transparent, String negativeBtn, String positiveBtn, String leftBtn, VoidCallback onLeftBtn, bool yearSelectAllowed}) async { initialDate ??= DateTime.now(); firstDate ??= DateTime(initialDate.year - 1); lastDate ??= DateTime(initialDate.year + 1); theme ??= ThemeData(); assert(initialDate != null); assert(firstDate != null); assert(lastDate != null); assert( !initialDate.isBefore(firstDate), 'initialDate must be on or after firstDate', ); assert( !initialDate.isAfter(lastDate), 'initialDate must be on or before lastDate', ); assert( !firstDate.isAfter(lastDate), 'lastDate must be on or after firstDate', ); assert( selectableDayPredicate == null || selectableDayPredicate(initialDate), 'Provided initialDate must satisfy provided selectableDayPredicate', ); assert( initialDatePickerMode != null, 'initialDatePickerMode must not be null', ); assert( (onLeftBtn != null && leftBtn != null) || onLeftBtn == null, "If you provide onLeftBtn, you must provide leftBtn", ); assert(context != null); assert(debugCheckHasMaterialLocalizations(context)); Widget child = GestureDetector( onTap: () { if (!barrierDismissible) { Navigator.pop(context); } }, child: Container( color: background, child: GestureDetector( onTap: () { // }, child: DatePickerDialog( initialDate: initialDate, firstDate: firstDate, lastDate: lastDate, yearSelectAllowed: yearSelectAllowed, selectableDayPredicate: selectableDayPredicate, initialDatePickerMode: initialDatePickerMode, era: era, locale: locale, borderRadius: borderRadius, imageHeader: imageHeader, description: description, fontFamily: fontFamily, negativeBtn: negativeBtn, positiveBtn: positiveBtn, leftBtn: leftBtn, onLeftBtn: onLeftBtn, ), ), ), ); if (textDirection != null) { child = Directionality( textDirection: textDirection, child: child, ); } if (locale != null) { child = Localizations.override( context: context, locale: locale, child: child, ); } return await showDialog( context: context, barrierDismissible: barrierDismissible, builder: (_) => Theme(data: theme, child: child), ); }