| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:math' as math; |
| |
| import 'package:flutter/gestures.dart' show DragStartBehavior; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import '../debug.dart'; |
| import '../icon_button.dart'; |
| import '../icons.dart'; |
| import '../ink_well.dart'; |
| import '../material.dart'; |
| import '../material_localizations.dart'; |
| import '../theme.dart'; |
| |
| import 'date_picker_common.dart'; |
| |
| // NOTE: this is the original implementation for the Material Date Picker. |
| // These classes are deprecated and the whole file can be removed after |
| // this has been on stable for long enough for people to migrate to the new |
| // CalendarDatePicker (if needed, as showDatePicker has already been migrated |
| // and it is what most apps would have used). |
| |
| |
| // Examples can assume: |
| // BuildContext context; |
| |
| 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); |
| |
| 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. |
| /// |
| @Deprecated( |
| 'Use CalendarDatePicker instead. ' |
| 'This feature was deprecated after v1.15.3.' |
| ) |
| 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, |
| }) : 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<DateTime> 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; |
| |
| /// 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<Widget> _getDayHeaders(TextStyle headerStyle, MaterialLocalizations localizations) { |
| final List<Widget> result = <Widget>[]; |
| 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: headerStyle)), |
| )); |
| if (i == (localizations.firstDayOfWeekIndex - 1) % 7) |
| break; |
| } |
| return result; |
| } |
| |
| // Do not use this directly - call getDaysInMonth instead. |
| static const List<int> _daysInMonth = <int>[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<Widget> labels = <Widget>[ |
| ..._getDayHeaders(themeData.textTheme.caption, 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; |
| |
| 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; |
| decoration = BoxDecoration( |
| color: themeData.accentColor, |
| shape: BoxShape.circle, |
| ); |
| } else if (disabled) { |
| itemStyle = themeData.textTheme.bodyText2.copyWith(color: themeData.disabledColor); |
| } 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); |
| } |
| |
| 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); |
| } |
| } |
| |
| return Padding( |
| padding: const EdgeInsets.symmetric(horizontal: 8.0), |
| child: Column( |
| children: <Widget>[ |
| Container( |
| height: _kDayPickerRowHeight, |
| child: Center( |
| child: ExcludeSemantics( |
| child: Text( |
| localizations.formatMonthYear(displayedMonth), |
| style: themeData.textTheme.subtitle1, |
| ), |
| ), |
| ), |
| ), |
| 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. |
| /// |
| @Deprecated( |
| 'Use CalendarDatePicker instead. ' |
| 'This feature was deprecated after v1.15.3.' |
| ) |
| 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, |
| }) : 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<DateTime> 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; |
| |
| @override |
| _MonthPickerState createState() => _MonthPickerState(); |
| } |
| |
| // ignore: deprecated_member_use_from_same_package |
| class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStateMixin { |
| static final Animatable<double> _chevronOpacityTween = Tween<double>(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 |
| // ignore: deprecated_member_use_from_same_package |
| 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<double> _chevronOpacityAnimation; |
| |
| 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(); |
| }); |
| }); |
| } |
| |
| 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); |
| // ignore: deprecated_member_use_from_same_package |
| return DayPicker( |
| key: ValueKey<DateTime>(month), |
| selectedDate: widget.selectedDate, |
| currentDate: _todayDate, |
| onChanged: widget.onChanged, |
| firstDate: widget.firstDate, |
| lastDate: widget.lastDate, |
| displayedMonth: month, |
| selectableDayPredicate: widget.selectableDayPredicate, |
| dragStartBehavior: widget.dragStartBehavior, |
| ); |
| } |
| |
| 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: <Widget>[ |
| Semantics( |
| sortKey: _MonthPickerSortKey.calendar, |
| child: NotificationListener<ScrollStartNotification>( |
| onNotification: (_) { |
| _chevronOpacityController.forward(); |
| return false; |
| }, |
| child: NotificationListener<ScrollEndNotification>( |
| onNotification: (_) { |
| _chevronOpacityController.reverse(); |
| return false; |
| }, |
| child: PageView.builder( |
| dragStartBehavior: widget.dragStartBehavior, |
| key: ValueKey<DateTime>(widget.selectedDate), |
| controller: _dayPickerController, |
| scrollDirection: Axis.horizontal, |
| itemCount: _monthDelta(widget.firstDate, widget.lastDate) + 1, |
| itemBuilder: _buildItems, |
| onPageChanged: _handleMonthPageChanged, |
| ), |
| ), |
| ), |
| ), |
| PositionedDirectional( |
| top: 0.0, |
| start: 8.0, |
| child: Semantics( |
| sortKey: _MonthPickerSortKey.previousMonth, |
| child: FadeTransition( |
| opacity: _chevronOpacityAnimation, |
| child: IconButton( |
| icon: const Icon(Icons.chevron_left), |
| tooltip: _isDisplayingFirstMonth ? null : '${localizations.previousMonthTooltip} ${localizations.formatMonthYear(_previousMonthDate)}', |
| onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth, |
| ), |
| ), |
| ), |
| ), |
| PositionedDirectional( |
| top: 0.0, |
| end: 8.0, |
| child: Semantics( |
| sortKey: _MonthPickerSortKey.nextMonth, |
| child: FadeTransition( |
| opacity: _chevronOpacityAnimation, |
| child: IconButton( |
| icon: const Icon(Icons.chevron_right), |
| 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. |
| /// |
| @Deprecated( |
| 'Use CalendarDatePicker instead. ' |
| 'This feature was deprecated after v1.15.3.' |
| ) |
| 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.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<DateTime> 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; |
| |
| @override |
| _YearPickerState createState() => _YearPickerState(); |
| } |
| |
| // ignore: deprecated_member_use_from_same_package |
| class _YearPickerState extends State<YearPicker> { |
| 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; |
| return ListView.builder( |
| 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) |
| : style; |
| return InkWell( |
| key: ValueKey<int>(year), |
| onTap: () { |
| widget.onChanged(DateTime(year, widget.selectedDate.month, widget.selectedDate.day)); |
| }, |
| child: Center( |
| child: Semantics( |
| selected: isSelected, |
| child: Text(year.toString(), style: itemStyle), |
| ), |
| ), |
| ); |
| }, |
| ); |
| } |
| } |