You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
PatientApp-KKUMC/lib/widgets/input/date_picker/date_picker.dart

1461 lines
44 KiB
Dart

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<DatePickerMode> 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: <Widget>[
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<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;
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<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: 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<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 = _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: <Widget>[
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<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;
/// Optional era year.
final EraMode era;
final Locale locale;
/// Font
final String fontFamily;
@override
_MonthPickerState createState() => _MonthPickerState();
}
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
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);
// 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<DateTime>(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: <Widget>[
Semantics(
sortKey: _MonthPickerSortKey.calendar,
child: NotificationListener<ScrollStartNotification>(
onNotification: (_) {
_chevronOpacityController.forward();
return false;
},
child: NotificationListener<ScrollEndNotification>(
onNotification: (_) {
_chevronOpacityController.reverse();
return false;
},
child: PageView.builder(
physics: BouncingScrollPhysics(),
dragStartBehavior: widget.dragStartBehavior,
key: ValueKey<DateTime>(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<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;
/// Era
final EraMode era;
/// Font
final String fontFamily;
@override
_YearPickerState createState() => _YearPickerState();
}
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
.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<int>(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<DatePickerDialog> {
@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: <Widget>[
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: <Widget>[
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: <Widget>[
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<DateTime> 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<DateTime> 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<DateTime>(
context: context,
barrierDismissible: barrierDismissible,
builder: (_) => Theme(data: theme, child: child),
);
}