12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065 |
- // 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:math' as math;
- import 'package:flutter/gestures.dart' show DragStartBehavior;
- import 'package:flutter/material.dart';
- import 'package:flutter/rendering.dart';
- import 'package:flutter/services.dart';
- import 'package:flyinsonolite/controls/custom/customcalendardatepicker.dart';
- import 'package:flyinsonolite/controls/custom/customdialog.dart';
- import 'package:flyinsonolite/controls/custom/customiconbutton.dart';
- import 'package:flyinsonolite/controls/custom/customtextbutton.dart';
- import 'package:flyinsonolite/controls/text/fistext.dart';
- import 'package:flyinsonolite/infrastructure/scale.dart';
- final Size _calendarPortraitDialogSize = Size(330.s, 518.s);
- final Size _calendarLandscapeDialogSize = Size(496.s, 346.s);
- final Size _inputPortraitDialogSize = Size(330.s, 270.s);
- final Size _inputLandscapeDialogSize = Size(496.s, 160.s);
- final Size _inputRangeLandscapeDialogSize = Size(496.s, 164.s);
- const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200);
- final double _inputFormPortraitHeight = 98.s;
- final double _inputFormLandscapeHeight = 108.s;
- /// Shows a dialog containing a Material Design date picker.
- ///
- /// The returned [Future] resolves to the date selected by the user when the
- /// user confirms the dialog. If the user cancels the dialog, null is returned.
- ///
- /// When the date picker is first displayed, it will show the month of
- /// [initialDate], with [initialDate] selected.
- ///
- /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest
- /// allowable date. [initialDate] must either fall between these dates,
- /// or be equal to one of them. For each of these [DateTime] parameters, only
- /// their dates are considered. Their time fields are ignored. They must all
- /// be non-null.
- ///
- /// The [currentDate] represents the current day (i.e. today). This
- /// date will be highlighted in the day grid. If null, the date of
- /// `DateTime.now()` will be used.
- ///
- /// An optional [initialEntryMode] argument can be used to display the date
- /// picker in the [DatePickerEntryMode.calendar] (a calendar month grid)
- /// or [DatePickerEntryMode.input] (a text input field) mode.
- /// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
- ///
- /// An optional [selectableDayPredicate] function can be passed in to only allow
- /// certain days for selection. If provided, only the days that
- /// [selectableDayPredicate] returns true for will be selectable. For example,
- /// this can be used to only allow weekdays for selection. If provided, it must
- /// return true for [initialDate].
- ///
- /// The following optional string parameters allow you to override the default
- /// text used for various parts of the dialog:
- ///
- /// * [helpText], label displayed at the top of the dialog.
- /// * [cancelText], label on the cancel button.
- /// * [confirmText], label on the ok button.
- /// * [errorFormatText], message used when the input text isn't in a proper date format.
- /// * [errorInvalidText], message used when the input text isn't a selectable date.
- /// * [fieldHintText], text used to prompt the user when no text has been entered in the field.
- /// * [fieldLabelText], label for the date text input field.
- ///
- /// 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
- /// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It
- /// defaults to the ambient text direction provided by [Directionality]. If both
- /// [locale] and [textDirection] are non-null, [textDirection] overrides the
- /// direction chosen for the [locale].
- ///
- /// The [context], [useRootNavigator] and [routeSettings] arguments are passed to
- /// [showCustomDialog], the documentation for which discusses how it is used. [context]
- /// and [useRootNavigator] must be non-null.
- ///
- /// The [builder] parameter can be used to wrap the dialog widget
- /// to add inherited widgets like [Theme].
- ///
- /// An optional [initialDatePickerMode] argument can be used to have the
- /// calendar date picker initially appear in the [DatePickerMode.year] or
- /// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day], and
- /// must be non-null.
- ///
- /// {@macro flutter.widgets.RawDialogRoute}
- ///
- /// ### State Restoration
- ///
- /// Using this method will not enable state restoration for the date picker.
- /// In order to enable state restoration for a date picker, use
- /// [Navigator.restorablePush] or [Navigator.restorablePushNamed] with
- /// [DatePickerDialog].
- ///
- /// For more information about state restoration, see [RestorationManager].
- ///
- /// {@macro flutter.widgets.RestorationManager}
- ///
- /// {@tool dartpad}
- /// This sample demonstrates how to create a restorable Material date picker.
- /// This is accomplished by enabling state restoration by specifying
- /// [MaterialApp.restorationScopeId] and using [Navigator.restorablePush] to
- /// push [DatePickerDialog] when the button is tapped.
- ///
- /// ** See code in examples/api/lib/material/date_picker/show_date_picker.0.dart **
- /// {@end-tool}
- ///
- /// See also:
- ///
- /// * [showDateRangePicker], which shows a Material Design date range picker
- /// used to select a range of dates.
- /// * [CustomCalendarDatePicker], which provides the calendar grid used by the date picker dialog.
- /// * [InputDatePickerFormField], which provides a text input field for entering dates.
- /// * [DisplayFeatureSubScreen], which documents the specifics of how
- /// [DisplayFeature]s can split the screen into sub-screens.
- /// * [showTimePicker], which shows a dialog that contains a Material Design time picker.
- ///
- Future<DateTime?> showCustomDatePicker({
- required BuildContext context,
- required DateTime initialDate,
- required DateTime firstDate,
- required DateTime lastDate,
- DateTime? currentDate,
- DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar,
- SelectableDayPredicate? selectableDayPredicate,
- String? helpText,
- String? cancelText,
- String? confirmText,
- Locale? locale,
- bool useRootNavigator = true,
- RouteSettings? routeSettings,
- TextDirection? textDirection,
- TransitionBuilder? builder,
- DatePickerMode initialDatePickerMode = DatePickerMode.day,
- String? errorFormatText,
- String? errorInvalidText,
- String? fieldHintText,
- String? fieldLabelText,
- TextInputType? keyboardType,
- Offset? anchorPoint,
- }) async {
- assert(context != null);
- assert(initialDate != null);
- assert(firstDate != null);
- assert(lastDate != null);
- initialDate = DateUtils.dateOnly(initialDate);
- firstDate = DateUtils.dateOnly(firstDate);
- lastDate = DateUtils.dateOnly(lastDate);
- assert(
- !lastDate.isBefore(firstDate),
- 'lastDate $lastDate must be on or after firstDate $firstDate.',
- );
- assert(
- !initialDate.isBefore(firstDate),
- 'initialDate $initialDate must be on or after firstDate $firstDate.',
- );
- assert(
- !initialDate.isAfter(lastDate),
- 'initialDate $initialDate must be on or before lastDate $lastDate.',
- );
- assert(
- selectableDayPredicate == null || selectableDayPredicate(initialDate),
- 'Provided initialDate $initialDate must satisfy provided selectableDayPredicate.',
- );
- assert(initialEntryMode != null);
- assert(useRootNavigator != null);
- assert(initialDatePickerMode != null);
- assert(debugCheckHasMaterialLocalizations(context));
- Widget dialog = DatePickerDialog(
- initialDate: initialDate,
- firstDate: firstDate,
- lastDate: lastDate,
- currentDate: currentDate,
- initialEntryMode: initialEntryMode,
- selectableDayPredicate: selectableDayPredicate,
- helpText: helpText,
- cancelText: cancelText,
- confirmText: confirmText,
- initialCalendarMode: initialDatePickerMode,
- errorFormatText: errorFormatText,
- errorInvalidText: errorInvalidText,
- fieldHintText: fieldHintText,
- fieldLabelText: fieldLabelText,
- keyboardType: keyboardType,
- );
- if (textDirection != null) {
- dialog = Directionality(
- textDirection: textDirection,
- child: dialog,
- );
- }
- if (locale != null) {
- dialog = Localizations.override(
- context: context,
- locale: locale,
- child: dialog,
- );
- }
- return showCustomDialog<DateTime>(
- context: context,
- useRootNavigator: useRootNavigator,
- routeSettings: routeSettings,
- builder: (BuildContext context) {
- return builder == null ? dialog : builder(context, dialog);
- },
- anchorPoint: anchorPoint,
- );
- }
- /// A Material-style date picker dialog.
- ///
- /// It is used internally by [showCustomDatePicker] or can be directly pushed
- /// onto the [Navigator] stack to enable state restoration. See
- /// [showCustomDatePicker] for a state restoration app example.
- ///
- /// See also:
- ///
- /// * [showCustomDatePicker], which is a way to display the date picker.
- class DatePickerDialog extends StatefulWidget {
- /// A Material-style date picker dialog.
- DatePickerDialog({
- super.key,
- required DateTime initialDate,
- required DateTime firstDate,
- required DateTime lastDate,
- DateTime? currentDate,
- this.initialEntryMode = DatePickerEntryMode.calendar,
- this.selectableDayPredicate,
- this.cancelText,
- this.confirmText,
- this.helpText,
- this.initialCalendarMode = DatePickerMode.day,
- this.errorFormatText,
- this.errorInvalidText,
- this.fieldHintText,
- this.fieldLabelText,
- this.keyboardType,
- this.restorationId,
- }) : assert(initialDate != null),
- assert(firstDate != null),
- assert(lastDate != null),
- initialDate = DateUtils.dateOnly(initialDate),
- firstDate = DateUtils.dateOnly(firstDate),
- lastDate = DateUtils.dateOnly(lastDate),
- currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
- assert(initialEntryMode != null),
- assert(initialCalendarMode != null) {
- assert(
- !this.lastDate.isBefore(this.firstDate),
- 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.',
- );
- assert(
- !this.initialDate.isBefore(this.firstDate),
- 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.',
- );
- assert(
- !this.initialDate.isAfter(this.lastDate),
- 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.',
- );
- assert(
- selectableDayPredicate == null ||
- selectableDayPredicate!(this.initialDate),
- 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate',
- );
- }
- /// The initially selected [DateTime] that the picker should display.
- final DateTime initialDate;
- /// The earliest allowable [DateTime] that the user can select.
- final DateTime firstDate;
- /// The latest allowable [DateTime] that the user can select.
- final DateTime lastDate;
- /// The [DateTime] representing today. It will be highlighted in the day grid.
- final DateTime currentDate;
- /// The initial mode of date entry method for the date picker dialog.
- ///
- /// See [DatePickerEntryMode] for more details on the different data entry
- /// modes available.
- final DatePickerEntryMode initialEntryMode;
- /// Function to provide full control over which [DateTime] can be selected.
- final SelectableDayPredicate? selectableDayPredicate;
- /// The text that is displayed on the cancel button.
- final String? cancelText;
- /// The text that is displayed on the confirm button.
- final String? confirmText;
- /// The text that is displayed at the top of the header.
- ///
- /// This is used to indicate to the user what they are selecting a date for.
- final String? helpText;
- /// The initial display of the calendar picker.
- final DatePickerMode initialCalendarMode;
- /// The error text displayed if the entered date is not in the correct format.
- final String? errorFormatText;
- /// The error text displayed if the date is not valid.
- ///
- /// A date is not valid if it is earlier than [firstDate], later than
- /// [lastDate], or doesn't pass the [selectableDayPredicate].
- final String? errorInvalidText;
- /// The hint text displayed in the [TextField].
- ///
- /// If this is null, it will default to the date format string. For example,
- /// 'mm/dd/yyyy' for en_US.
- final String? fieldHintText;
- /// The label text displayed in the [TextField].
- ///
- /// If this is null, it will default to the words representing the date format
- /// string. For example, 'Month, Day, Year' for en_US.
- final String? fieldLabelText;
- /// The keyboard type of the [TextField].
- ///
- /// If this is null, it will default to [TextInputType.datetime]
- final TextInputType? keyboardType;
- /// Restoration ID to save and restore the state of the [DatePickerDialog].
- ///
- /// If it is non-null, the date picker will persist and restore the
- /// date selected on the dialog.
- ///
- /// The state of this widget is persisted in a [RestorationBucket] claimed
- /// from the surrounding [RestorationScope] using the provided restoration ID.
- ///
- /// See also:
- ///
- /// * [RestorationManager], which explains how state restoration works in
- /// Flutter.
- final String? restorationId;
- @override
- State<DatePickerDialog> createState() => _DatePickerDialogState();
- }
- class _DatePickerDialogState extends State<DatePickerDialog>
- with RestorationMixin {
- late final RestorableDateTime _selectedDate =
- RestorableDateTime(widget.initialDate);
- late final _RestorableDatePickerEntryMode _entryMode =
- _RestorableDatePickerEntryMode(widget.initialEntryMode);
- final _RestorableAutovalidateMode _autovalidateMode =
- _RestorableAutovalidateMode(AutovalidateMode.disabled);
- @override
- String? get restorationId => widget.restorationId;
- @override
- void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
- registerForRestoration(_selectedDate, 'selected_date');
- registerForRestoration(_autovalidateMode, 'autovalidateMode');
- registerForRestoration(_entryMode, 'calendar_entry_mode');
- }
- final GlobalKey _calendarPickerKey = GlobalKey();
- final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
- void _handleOk() {
- if (_entryMode.value == DatePickerEntryMode.input ||
- _entryMode.value == DatePickerEntryMode.inputOnly) {
- final FormState form = _formKey.currentState!;
- if (!form.validate()) {
- setState(() => _autovalidateMode.value = AutovalidateMode.always);
- return;
- }
- form.save();
- }
- Navigator.pop(context, _selectedDate.value);
- }
- void _handleCancel() {
- Navigator.pop(context);
- }
- void _handleEntryModeToggle() {
- setState(() {
- switch (_entryMode.value) {
- case DatePickerEntryMode.calendar:
- _autovalidateMode.value = AutovalidateMode.disabled;
- _entryMode.value = DatePickerEntryMode.input;
- break;
- case DatePickerEntryMode.input:
- _formKey.currentState!.save();
- _entryMode.value = DatePickerEntryMode.calendar;
- break;
- case DatePickerEntryMode.calendarOnly:
- case DatePickerEntryMode.inputOnly:
- assert(false, 'Can not change entry mode from _entryMode');
- break;
- }
- });
- }
- void _handleDateChanged(DateTime date) {
- setState(() {
- _selectedDate.value = date;
- });
- }
- Size _dialogSize(BuildContext context) {
- final Orientation orientation = MediaQuery.of(context).orientation;
- switch (_entryMode.value) {
- case DatePickerEntryMode.calendar:
- case DatePickerEntryMode.calendarOnly:
- switch (orientation) {
- case Orientation.portrait:
- return _calendarPortraitDialogSize;
- case Orientation.landscape:
- return _calendarLandscapeDialogSize;
- }
- case DatePickerEntryMode.input:
- case DatePickerEntryMode.inputOnly:
- switch (orientation) {
- case Orientation.portrait:
- return _inputPortraitDialogSize;
- case Orientation.landscape:
- return _inputLandscapeDialogSize;
- }
- }
- }
- static const Map<ShortcutActivator, Intent> _formShortcutMap =
- <ShortcutActivator, Intent>{
- // Pressing enter on the field will move focus to the next field or control.
- SingleActivator(LogicalKeyboardKey.enter): NextFocusIntent(),
- };
- @override
- Widget build(BuildContext context) {
- final ThemeData theme = Theme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final Orientation orientation = MediaQuery.of(context).orientation;
- final TextTheme textTheme = theme.textTheme;
- // Constrain the textScaleFactor to the largest supported value to prevent
- // layout issues.
- final double textScaleFactor =
- math.min(MediaQuery.of(context).textScaleFactor, 1.3);
- final String dateText = localizations.formatMediumDate(_selectedDate.value);
- final Color onPrimarySurface = colorScheme.brightness == Brightness.light
- ? colorScheme.onPrimary
- : colorScheme.onSurface;
- final TextStyle? dateStyle = orientation == Orientation.landscape
- ? textTheme.headlineSmall?.copyWith(color: onPrimarySurface)
- : textTheme.headlineMedium?.copyWith(color: onPrimarySurface);
- final Widget actions = Container(
- alignment: AlignmentDirectional.centerEnd,
- constraints: BoxConstraints(minHeight: 52.s),
- padding: EdgeInsets.symmetric(horizontal: 8.s),
- child: OverflowBar(
- spacing: 8.s,
- children: <Widget>[
- CustomTextButton(
- onPressed: _handleCancel,
- child:
- FISText(widget.cancelText ?? localizations.cancelButtonLabel),
- ),
- CustomTextButton(
- onPressed: _handleOk,
- child: FISText(widget.confirmText ?? localizations.okButtonLabel),
- ),
- ],
- ),
- );
- CustomCalendarDatePicker calendarDatePicker() {
- return CustomCalendarDatePicker(
- key: _calendarPickerKey,
- initialDate: _selectedDate.value,
- firstDate: widget.firstDate,
- lastDate: widget.lastDate,
- currentDate: widget.currentDate,
- onDateChanged: _handleDateChanged,
- selectableDayPredicate: widget.selectableDayPredicate,
- initialCalendarMode: widget.initialCalendarMode,
- );
- }
- Form inputDatePicker() {
- return Form(
- key: _formKey,
- autovalidateMode: _autovalidateMode.value,
- child: Container(
- padding: EdgeInsets.symmetric(horizontal: 24.s),
- height: orientation == Orientation.portrait
- ? _inputFormPortraitHeight
- : _inputFormLandscapeHeight,
- child: Shortcuts(
- shortcuts: _formShortcutMap,
- child: Column(
- children: <Widget>[
- const Spacer(),
- InputDatePickerFormField(
- initialDate: _selectedDate.value,
- firstDate: widget.firstDate,
- lastDate: widget.lastDate,
- onDateSubmitted: _handleDateChanged,
- onDateSaved: _handleDateChanged,
- selectableDayPredicate: widget.selectableDayPredicate,
- errorFormatText: widget.errorFormatText,
- errorInvalidText: widget.errorInvalidText,
- fieldHintText: widget.fieldHintText,
- fieldLabelText: widget.fieldLabelText,
- keyboardType: widget.keyboardType,
- autofocus: true,
- ),
- const Spacer(),
- ],
- ),
- ),
- ),
- );
- }
- final Widget picker;
- final Widget? entryModeButton;
- switch (_entryMode.value) {
- case DatePickerEntryMode.calendar:
- picker = calendarDatePicker();
- entryModeButton = CustomIconButton(
- icon: Icon(
- Icons.edit,
- size: 24.s,
- ),
- constraints: const BoxConstraints(),
- padding: const EdgeInsets.all(0),
- color: onPrimarySurface,
- tooltip: localizations.inputDateModeButtonLabel,
- onPressed: _handleEntryModeToggle,
- );
- break;
- case DatePickerEntryMode.calendarOnly:
- picker = calendarDatePicker();
- entryModeButton = null;
- break;
- case DatePickerEntryMode.input:
- picker = inputDatePicker();
- entryModeButton = CustomIconButton(
- icon: Icon(
- Icons.calendar_today,
- size: 24.s,
- ),
- constraints: const BoxConstraints(),
- padding: const EdgeInsets.all(0),
- color: onPrimarySurface,
- tooltip: localizations.calendarModeButtonLabel,
- onPressed: _handleEntryModeToggle,
- );
- break;
- case DatePickerEntryMode.inputOnly:
- picker = inputDatePicker();
- entryModeButton = null;
- break;
- }
- final Widget header = _DatePickerHeader(
- helpText: widget.helpText ?? localizations.datePickerHelpText,
- titleText: dateText,
- titleStyle: dateStyle,
- orientation: orientation,
- isShort: orientation == Orientation.landscape,
- entryModeButton: entryModeButton,
- );
- final Size dialogSize = _dialogSize(context) * textScaleFactor;
- return CustomDialog(
- insetPadding: EdgeInsets.symmetric(horizontal: 16.s, vertical: 24.s),
- clipBehavior: Clip.antiAlias,
- child: AnimatedContainer(
- width: dialogSize.width,
- height: dialogSize.height,
- duration: _dialogSizeAnimationDuration,
- curve: Curves.easeIn,
- child: MediaQuery(
- data: MediaQuery.of(context).copyWith(
- textScaleFactor: textScaleFactor,
- ),
- child: Builder(builder: (BuildContext context) {
- switch (orientation) {
- case Orientation.portrait:
- return Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: <Widget>[
- header,
- Expanded(child: picker),
- actions,
- ],
- );
- case Orientation.landscape:
- return Row(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: <Widget>[
- header,
- Flexible(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: <Widget>[
- Expanded(child: picker),
- actions,
- ],
- ),
- ),
- ],
- );
- }
- }),
- ),
- ),
- );
- }
- }
- // A restorable [DatePickerEntryMode] value.
- //
- // This serializes each entry as a unique `int` value.
- class _RestorableDatePickerEntryMode
- extends RestorableValue<DatePickerEntryMode> {
- _RestorableDatePickerEntryMode(
- DatePickerEntryMode defaultValue,
- ) : _defaultValue = defaultValue;
- final DatePickerEntryMode _defaultValue;
- @override
- DatePickerEntryMode createDefaultValue() => _defaultValue;
- @override
- void didUpdateValue(DatePickerEntryMode? oldValue) {
- assert(debugIsSerializableForRestoration(value.index));
- notifyListeners();
- }
- @override
- DatePickerEntryMode fromPrimitives(Object? data) =>
- DatePickerEntryMode.values[data! as int];
- @override
- Object? toPrimitives() => value.index;
- }
- // A restorable [AutovalidateMode] value.
- //
- // This serializes each entry as a unique `int` value.
- class _RestorableAutovalidateMode extends RestorableValue<AutovalidateMode> {
- _RestorableAutovalidateMode(
- AutovalidateMode defaultValue,
- ) : _defaultValue = defaultValue;
- final AutovalidateMode _defaultValue;
- @override
- AutovalidateMode createDefaultValue() => _defaultValue;
- @override
- void didUpdateValue(AutovalidateMode? oldValue) {
- assert(debugIsSerializableForRestoration(value.index));
- notifyListeners();
- }
- @override
- AutovalidateMode fromPrimitives(Object? data) =>
- AutovalidateMode.values[data! as int];
- @override
- Object? toPrimitives() => value.index;
- }
- /// Re-usable widget that displays the selected date (in large font) and the
- /// help text above it.
- ///
- /// These types include:
- ///
- /// * Single Date picker with calendar mode.
- /// * Single Date picker with text input mode.
- /// * Date Range picker with text input mode.
- ///
- /// [helpText], [orientation], [icon], [onIconPressed] are required and must be
- /// non-null.
- class _DatePickerHeader extends StatelessWidget {
- /// Creates a header for use in a date picker dialog.
- const _DatePickerHeader({
- required this.helpText,
- required this.titleText,
- this.titleSemanticsLabel,
- required this.titleStyle,
- required this.orientation,
- this.isShort = false,
- this.entryModeButton,
- }) : assert(helpText != null),
- assert(orientation != null),
- assert(isShort != null);
- double get _datePickerHeaderLandscapeWidth => 152.s;
- double get _datePickerHeaderPortraitHeight => 120.s;
- double get _headerPaddingLandscape => 16.s;
- /// The text that is displayed at the top of the header.
- ///
- /// This is used to indicate to the user what they are selecting a date for.
- final String helpText;
- /// The text that is displayed at the center of the header.
- final String titleText;
- /// The semantic label associated with the [titleText].
- final String? titleSemanticsLabel;
- /// The [TextStyle] that the title text is displayed with.
- final TextStyle? titleStyle;
- /// The orientation is used to decide how to layout its children.
- final Orientation orientation;
- /// Indicates the header is being displayed in a shorter/narrower context.
- ///
- /// This will be used to tighten up the space between the help text and date
- /// text if `true`. Additionally, it will use a smaller typography style if
- /// `true`.
- ///
- /// This is necessary for displaying the manual input mode in
- /// landscape orientation, in order to account for the keyboard height.
- final bool isShort;
- final Widget? entryModeButton;
- @override
- Widget build(BuildContext context) {
- final ThemeData theme = Theme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
- final TextTheme textTheme = theme.textTheme;
- // The header should use the primary color in light themes and surface color in dark
- final bool isDark = colorScheme.brightness == Brightness.dark;
- final Color primarySurfaceColor =
- isDark ? colorScheme.surface : colorScheme.primary;
- final Color onPrimarySurfaceColor =
- isDark ? colorScheme.onSurface : colorScheme.onPrimary;
- final TextStyle? helpStyle = textTheme.labelSmall
- ?.copyWith(color: onPrimarySurfaceColor, letterSpacing: 1.5.s);
- final Text help = Text(
- helpText,
- style: helpStyle,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- );
- final Text title = Text(
- titleText,
- semanticsLabel: titleSemanticsLabel ?? titleText,
- style: titleStyle,
- maxLines: orientation == Orientation.portrait ? 1 : 2,
- overflow: TextOverflow.ellipsis,
- );
- switch (orientation) {
- case Orientation.portrait:
- return SizedBox(
- height: _datePickerHeaderPortraitHeight,
- child: Material(
- color: primarySurfaceColor,
- child: Padding(
- padding: EdgeInsetsDirectional.only(
- start: 24.s,
- end: 12.s,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: <Widget>[
- SizedBox(height: 16.s),
- help,
- Flexible(child: SizedBox(height: 38.s)),
- Row(
- children: <Widget>[
- Expanded(child: title),
- if (entryModeButton != null) entryModeButton!,
- ],
- ),
- ],
- ),
- ),
- ),
- );
- case Orientation.landscape:
- return SizedBox(
- width: _datePickerHeaderLandscapeWidth,
- child: Material(
- color: primarySurfaceColor,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: <Widget>[
- SizedBox(height: 16.s),
- Padding(
- padding: EdgeInsets.symmetric(
- horizontal: _headerPaddingLandscape,
- ),
- child: help,
- ),
- SizedBox(height: isShort ? 16.s : 56.s),
- Expanded(
- child: Padding(
- padding: EdgeInsets.symmetric(
- horizontal: _headerPaddingLandscape,
- ),
- child: title,
- ),
- ),
- if (entryModeButton != null)
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 4.s),
- child: entryModeButton,
- ),
- ],
- ),
- ),
- );
- }
- }
- }
- /// Shows a full screen modal dialog containing a Material Design date range
- /// picker.
- ///
- /// The returned [Future] resolves to the [DateTimeRange] selected by the user
- /// when the user saves their selection. If the user cancels the dialog, null is
- /// returned.
- ///
- /// If [initialDateRange] is non-null, then it will be used as the initially
- /// selected date range. If it is provided, `initialDateRange.start` must be
- /// before or on `initialDateRange.end`.
- ///
- /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest
- /// allowable date. Both must be non-null.
- ///
- /// If an initial date range is provided, `initialDateRange.start`
- /// and `initialDateRange.end` must both fall between or on [firstDate] and
- /// [lastDate]. For all of these [DateTime] values, only their dates are
- /// considered. Their time fields are ignored.
- ///
- /// The [currentDate] represents the current day (i.e. today). This
- /// date will be highlighted in the day grid. If null, the date of
- /// `DateTime.now()` will be used.
- ///
- /// An optional [initialEntryMode] argument can be used to display the date
- /// picker in the [DatePickerEntryMode.calendar] (a scrollable calendar month
- /// grid) or [DatePickerEntryMode.input] (two text input fields) mode.
- /// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
- ///
- /// The following optional string parameters allow you to override the default
- /// text used for various parts of the dialog:
- ///
- /// * [helpText], the label displayed at the top of the dialog.
- /// * [cancelText], the label on the cancel button for the text input mode.
- /// * [confirmText],the label on the ok button for the text input mode.
- /// * [saveText], the label on the save button for the fullscreen calendar
- /// mode.
- /// * [errorFormatText], the message used when an input text isn't in a proper
- /// date format.
- /// * [errorInvalidText], the message used when an input text isn't a
- /// selectable date.
- /// * [errorInvalidRangeText], the message used when the date range is
- /// invalid (e.g. start date is after end date).
- /// * [fieldStartHintText], the text used to prompt the user when no text has
- /// been entered in the start field.
- /// * [fieldEndHintText], the text used to prompt the user when no text has
- /// been entered in the end field.
- /// * [fieldStartLabelText], the label for the start date text input field.
- /// * [fieldEndLabelText], the label for the end date text input field.
- ///
- /// 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
- /// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It
- /// defaults to the ambient text direction provided by [Directionality]. If both
- /// [locale] and [textDirection] are non-null, [textDirection] overrides the
- /// direction chosen for the [locale].
- ///
- /// The [context], [useRootNavigator] and [routeSettings] arguments are passed
- /// to [showCustomDialog], the documentation for which discusses how it is used.
- /// [context] and [useRootNavigator] must be non-null.
- ///
- /// The [builder] parameter can be used to wrap the dialog widget
- /// to add inherited widgets like [Theme].
- ///
- /// {@macro flutter.widgets.RawDialogRoute}
- ///
- /// ### State Restoration
- ///
- /// Using this method will not enable state restoration for the date range picker.
- /// In order to enable state restoration for a date range picker, use
- /// [Navigator.restorablePush] or [Navigator.restorablePushNamed] with
- /// [DateRangePickerDialog].
- ///
- /// For more information about state restoration, see [RestorationManager].
- ///
- /// {@macro flutter.widgets.RestorationManager}
- ///
- /// {@tool sample}
- /// This sample demonstrates how to create a restorable Material date range picker.
- /// This is accomplished by enabling state restoration by specifying
- /// [MaterialApp.restorationScopeId] and using [Navigator.restorablePush] to
- /// push [DateRangePickerDialog] when the button is tapped.
- ///
- /// ** See code in examples/api/lib/material/date_picker/show_date_range_picker.0.dart **
- /// {@end-tool}
- ///
- /// See also:
- ///
- /// * [showCustomDatePicker], which shows a Material Design date picker used to
- /// select a single date.
- /// * [DateTimeRange], which is used to describe a date range.
- /// * [DisplayFeatureSubScreen], which documents the specifics of how
- /// [DisplayFeature]s can split the screen into sub-screens.
- Future<DateTimeRange?> showDateRangePicker({
- required BuildContext context,
- DateTimeRange? initialDateRange,
- required DateTime firstDate,
- required DateTime lastDate,
- DateTime? currentDate,
- DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar,
- String? helpText,
- String? cancelText,
- String? confirmText,
- String? saveText,
- String? errorFormatText,
- String? errorInvalidText,
- String? errorInvalidRangeText,
- String? fieldStartHintText,
- String? fieldEndHintText,
- String? fieldStartLabelText,
- String? fieldEndLabelText,
- Locale? locale,
- bool useRootNavigator = true,
- RouteSettings? routeSettings,
- TextDirection? textDirection,
- TransitionBuilder? builder,
- Offset? anchorPoint,
- }) async {
- assert(context != null);
- assert(
- initialDateRange == null ||
- (initialDateRange.start != null && initialDateRange.end != null),
- 'initialDateRange must be null or have non-null start and end dates.',
- );
- assert(
- initialDateRange == null ||
- !initialDateRange.start.isAfter(initialDateRange.end),
- "initialDateRange's start date must not be after it's end date.",
- );
- initialDateRange =
- initialDateRange == null ? null : DateUtils.datesOnly(initialDateRange);
- assert(firstDate != null);
- firstDate = DateUtils.dateOnly(firstDate);
- assert(lastDate != null);
- lastDate = DateUtils.dateOnly(lastDate);
- assert(
- !lastDate.isBefore(firstDate),
- 'lastDate $lastDate must be on or after firstDate $firstDate.',
- );
- assert(
- initialDateRange == null || !initialDateRange.start.isBefore(firstDate),
- "initialDateRange's start date must be on or after firstDate $firstDate.",
- );
- assert(
- initialDateRange == null || !initialDateRange.end.isBefore(firstDate),
- "initialDateRange's end date must be on or after firstDate $firstDate.",
- );
- assert(
- initialDateRange == null || !initialDateRange.start.isAfter(lastDate),
- "initialDateRange's start date must be on or before lastDate $lastDate.",
- );
- assert(
- initialDateRange == null || !initialDateRange.end.isAfter(lastDate),
- "initialDateRange's end date must be on or before lastDate $lastDate.",
- );
- currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now());
- assert(initialEntryMode != null);
- assert(useRootNavigator != null);
- assert(debugCheckHasMaterialLocalizations(context));
- Widget dialog = DateRangePickerDialog(
- initialDateRange: initialDateRange,
- firstDate: firstDate,
- lastDate: lastDate,
- currentDate: currentDate,
- initialEntryMode: initialEntryMode,
- helpText: helpText,
- cancelText: cancelText,
- confirmText: confirmText,
- saveText: saveText,
- errorFormatText: errorFormatText,
- errorInvalidText: errorInvalidText,
- errorInvalidRangeText: errorInvalidRangeText,
- fieldStartHintText: fieldStartHintText,
- fieldEndHintText: fieldEndHintText,
- fieldStartLabelText: fieldStartLabelText,
- fieldEndLabelText: fieldEndLabelText,
- );
- if (textDirection != null) {
- dialog = Directionality(
- textDirection: textDirection,
- child: dialog,
- );
- }
- if (locale != null) {
- dialog = Localizations.override(
- context: context,
- locale: locale,
- child: dialog,
- );
- }
- return showCustomDialog<DateTimeRange>(
- context: context,
- useRootNavigator: useRootNavigator,
- routeSettings: routeSettings,
- useSafeArea: false,
- builder: (BuildContext context) {
- return builder == null ? dialog : builder(context, dialog);
- },
- anchorPoint: anchorPoint,
- );
- }
- /// Returns a locale-appropriate string to describe the start of a date range.
- ///
- /// If `startDate` is null, then it defaults to 'Start Date', otherwise if it
- /// is in the same year as the `endDate` then it will use the short month
- /// day format (i.e. 'Jan 21'). Otherwise it will return the short date format
- /// (i.e. 'Jan 21, 2020').
- String _formatRangeStartDate(MaterialLocalizations localizations,
- DateTime? startDate, DateTime? endDate) {
- return startDate == null
- ? localizations.dateRangeStartLabel
- : (endDate == null || startDate.year == endDate.year)
- ? localizations.formatShortMonthDay(startDate)
- : localizations.formatShortDate(startDate);
- }
- /// Returns an locale-appropriate string to describe the end of a date range.
- ///
- /// If `endDate` is null, then it defaults to 'End Date', otherwise if it
- /// is in the same year as the `startDate` and the `currentDate` then it will
- /// just use the short month day format (i.e. 'Jan 21'), otherwise it will
- /// include the year (i.e. 'Jan 21, 2020').
- String _formatRangeEndDate(MaterialLocalizations localizations,
- DateTime? startDate, DateTime? endDate, DateTime currentDate) {
- return endDate == null
- ? localizations.dateRangeEndLabel
- : (startDate != null &&
- startDate.year == endDate.year &&
- startDate.year == currentDate.year)
- ? localizations.formatShortMonthDay(endDate)
- : localizations.formatShortDate(endDate);
- }
- /// A Material-style date range picker dialog.
- ///
- /// It is used internally by [showDateRangePicker] or can be directly pushed
- /// onto the [Navigator] stack to enable state restoration. See
- /// [showDateRangePicker] for a state restoration app example.
- ///
- /// See also:
- ///
- /// * [showDateRangePicker], which is a way to display the date picker.
- class DateRangePickerDialog extends StatefulWidget {
- /// A Material-style date range picker dialog.
- const DateRangePickerDialog({
- super.key,
- this.initialDateRange,
- required this.firstDate,
- required this.lastDate,
- this.currentDate,
- this.initialEntryMode = DatePickerEntryMode.calendar,
- this.helpText,
- this.cancelText,
- this.confirmText,
- this.saveText,
- this.errorInvalidRangeText,
- this.errorFormatText,
- this.errorInvalidText,
- this.fieldStartHintText,
- this.fieldEndHintText,
- this.fieldStartLabelText,
- this.fieldEndLabelText,
- this.restorationId,
- });
- /// The date range that the date range picker starts with when it opens.
- ///
- /// If an initial date range is provided, `initialDateRange.start`
- /// and `initialDateRange.end` must both fall between or on [firstDate] and
- /// [lastDate]. For all of these [DateTime] values, only their dates are
- /// considered. Their time fields are ignored.
- ///
- /// If [initialDateRange] is non-null, then it will be used as the initially
- /// selected date range. If it is provided, `initialDateRange.start` must be
- /// before or on `initialDateRange.end`.
- final DateTimeRange? initialDateRange;
- /// The earliest allowable date on the date range.
- final DateTime firstDate;
- /// The latest allowable date on the date range.
- final DateTime lastDate;
- /// The [currentDate] represents the current day (i.e. today).
- ///
- /// This date will be highlighted in the day grid.
- ///
- /// If `null`, the date of `DateTime.now()` will be used.
- final DateTime? currentDate;
- /// The initial date range picker entry mode.
- ///
- /// The date range has two main modes: [DatePickerEntryMode.calendar] (a
- /// scrollable calendar month grid) or [DatePickerEntryMode.input] (two text
- /// input fields) mode.
- ///
- /// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
- final DatePickerEntryMode initialEntryMode;
- /// The label on the cancel button for the text input mode.
- ///
- /// If null, the localized value of
- /// [MaterialLocalizations.cancelButtonLabel] is used.
- final String? cancelText;
- /// The label on the "OK" button for the text input mode.
- ///
- /// If null, the localized value of
- /// [MaterialLocalizations.okButtonLabel] is used.
- final String? confirmText;
- /// The label on the save button for the fullscreen calendar mode.
- ///
- /// If null, the localized value of
- /// [MaterialLocalizations.saveButtonLabel] is used.
- final String? saveText;
- /// The label displayed at the top of the dialog.
- ///
- /// If null, the localized value of
- /// [MaterialLocalizations.dateRangePickerHelpText] is used.
- final String? helpText;
- /// The message used when the date range is invalid (e.g. start date is after
- /// end date).
- ///
- /// If null, the localized value of
- /// [MaterialLocalizations.invalidDateRangeLabel] is used.
- final String? errorInvalidRangeText;
- /// The message used when an input text isn't in a proper date format.
- ///
- /// If null, the localized value of
- /// [MaterialLocalizations.invalidDateFormatLabel] is used.
- final String? errorFormatText;
- /// The message used when an input text isn't a selectable date.
- ///
- /// If null, the localized value of
- /// [MaterialLocalizations.dateOutOfRangeLabel] is used.
- final String? errorInvalidText;
- /// The text used to prompt the user when no text has been entered in the
- /// start field.
- ///
- /// If null, the localized value of
- /// [MaterialLocalizations.dateHelpText] is used.
- final String? fieldStartHintText;
- /// The text used to prompt the user when no text has been entered in the
- /// end field.
- ///
- /// If null, the localized value of [MaterialLocalizations.dateHelpText] is
- /// used.
- final String? fieldEndHintText;
- /// The label for the start date text input field.
- ///
- /// If null, the localized value of [MaterialLocalizations.dateRangeStartLabel]
- /// is used.
- final String? fieldStartLabelText;
- /// The label for the end date text input field.
- ///
- /// If null, the localized value of [MaterialLocalizations.dateRangeEndLabel]
- /// is used.
- final String? fieldEndLabelText;
- /// Restoration ID to save and restore the state of the [DateRangePickerDialog].
- ///
- /// If it is non-null, the date range picker will persist and restore the
- /// date range selected on the dialog.
- ///
- /// The state of this widget is persisted in a [RestorationBucket] claimed
- /// from the surrounding [RestorationScope] using the provided restoration ID.
- ///
- /// See also:
- ///
- /// * [RestorationManager], which explains how state restoration works in
- /// Flutter.
- final String? restorationId;
- @override
- State<DateRangePickerDialog> createState() => _DateRangePickerDialogState();
- }
- class _DateRangePickerDialogState extends State<DateRangePickerDialog>
- with RestorationMixin {
- late final _RestorableDatePickerEntryMode _entryMode =
- _RestorableDatePickerEntryMode(widget.initialEntryMode);
- late final RestorableDateTimeN _selectedStart =
- RestorableDateTimeN(widget.initialDateRange?.start);
- late final RestorableDateTimeN _selectedEnd =
- RestorableDateTimeN(widget.initialDateRange?.end);
- final RestorableBool _autoValidate = RestorableBool(false);
- final GlobalKey _calendarPickerKey = GlobalKey();
- final GlobalKey<_InputDateRangePickerState> _inputPickerKey =
- GlobalKey<_InputDateRangePickerState>();
- @override
- String? get restorationId => widget.restorationId;
- @override
- void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
- registerForRestoration(_entryMode, 'entry_mode');
- registerForRestoration(_selectedStart, 'selected_start');
- registerForRestoration(_selectedEnd, 'selected_end');
- registerForRestoration(_autoValidate, 'autovalidate');
- }
- void _handleOk() {
- if (_entryMode.value == DatePickerEntryMode.input ||
- _entryMode.value == DatePickerEntryMode.inputOnly) {
- final _InputDateRangePickerState picker = _inputPickerKey.currentState!;
- if (!picker.validate()) {
- setState(() {
- _autoValidate.value = true;
- });
- return;
- }
- }
- final DateTimeRange? selectedRange = _hasSelectedDateRange
- ? DateTimeRange(start: _selectedStart.value!, end: _selectedEnd.value!)
- : null;
- Navigator.pop(context, selectedRange);
- }
- void _handleCancel() {
- Navigator.pop(context);
- }
- void _handleEntryModeToggle() {
- setState(() {
- switch (_entryMode.value) {
- case DatePickerEntryMode.calendar:
- _autoValidate.value = false;
- _entryMode.value = DatePickerEntryMode.input;
- break;
- case DatePickerEntryMode.input:
- // Validate the range dates
- if (_selectedStart.value != null &&
- (_selectedStart.value!.isBefore(widget.firstDate) ||
- _selectedStart.value!.isAfter(widget.lastDate))) {
- _selectedStart.value = null;
- // With no valid start date, having an end date makes no sense for the UI.
- _selectedEnd.value = null;
- }
- if (_selectedEnd.value != null &&
- (_selectedEnd.value!.isBefore(widget.firstDate) ||
- _selectedEnd.value!.isAfter(widget.lastDate))) {
- _selectedEnd.value = null;
- }
- // If invalid range (start after end), then just use the start date
- if (_selectedStart.value != null &&
- _selectedEnd.value != null &&
- _selectedStart.value!.isAfter(_selectedEnd.value!)) {
- _selectedEnd.value = null;
- }
- _entryMode.value = DatePickerEntryMode.calendar;
- break;
- case DatePickerEntryMode.calendarOnly:
- case DatePickerEntryMode.inputOnly:
- assert(false, 'Can not change entry mode from $_entryMode');
- break;
- }
- });
- }
- void _handleStartDateChanged(DateTime? date) {
- setState(() => _selectedStart.value = date);
- }
- void _handleEndDateChanged(DateTime? date) {
- setState(() => _selectedEnd.value = date);
- }
- bool get _hasSelectedDateRange =>
- _selectedStart.value != null && _selectedEnd.value != null;
- @override
- Widget build(BuildContext context) {
- final MediaQueryData mediaQuery = MediaQuery.of(context);
- final Orientation orientation = mediaQuery.orientation;
- final double textScaleFactor = math.min(mediaQuery.textScaleFactor, 1.3);
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final ColorScheme colors = Theme.of(context).colorScheme;
- final Color onPrimarySurface = colors.brightness == Brightness.light
- ? colors.onPrimary
- : colors.onSurface;
- final Widget contents;
- final Size size;
- ShapeBorder? shape;
- final double elevation;
- final EdgeInsets insetPadding;
- final bool showEntryModeButton =
- _entryMode.value == DatePickerEntryMode.calendar ||
- _entryMode.value == DatePickerEntryMode.input;
- switch (_entryMode.value) {
- case DatePickerEntryMode.calendar:
- case DatePickerEntryMode.calendarOnly:
- contents = _CalendarRangePickerDialog(
- key: _calendarPickerKey,
- selectedStartDate: _selectedStart.value,
- selectedEndDate: _selectedEnd.value,
- firstDate: widget.firstDate,
- lastDate: widget.lastDate,
- currentDate: widget.currentDate,
- onStartDateChanged: _handleStartDateChanged,
- onEndDateChanged: _handleEndDateChanged,
- onConfirm: _hasSelectedDateRange ? _handleOk : null,
- onCancel: _handleCancel,
- entryModeButton: showEntryModeButton
- ? CustomIconButton(
- icon: Icon(
- Icons.edit,
- size: 24.s,
- ),
- constraints: const BoxConstraints(),
- padding: EdgeInsets.zero,
- color: onPrimarySurface,
- tooltip: localizations.inputDateModeButtonLabel,
- onPressed: _handleEntryModeToggle,
- )
- : null,
- confirmText: widget.saveText ?? localizations.saveButtonLabel,
- helpText: widget.helpText ?? localizations.dateRangePickerHelpText,
- );
- size = mediaQuery.size;
- insetPadding = EdgeInsets.zero;
- shape = const RoundedRectangleBorder();
- elevation = 0;
- break;
- case DatePickerEntryMode.input:
- case DatePickerEntryMode.inputOnly:
- contents = _InputDateRangePickerDialog(
- selectedStartDate: _selectedStart.value,
- selectedEndDate: _selectedEnd.value,
- currentDate: widget.currentDate,
- picker: Container(
- padding: EdgeInsets.symmetric(horizontal: 24.s),
- height: orientation == Orientation.portrait
- ? _inputFormPortraitHeight
- : _inputFormLandscapeHeight,
- child: Column(
- children: <Widget>[
- const Spacer(),
- _InputDateRangePicker(
- key: _inputPickerKey,
- initialStartDate: _selectedStart.value,
- initialEndDate: _selectedEnd.value,
- firstDate: widget.firstDate,
- lastDate: widget.lastDate,
- onStartDateChanged: _handleStartDateChanged,
- onEndDateChanged: _handleEndDateChanged,
- autofocus: true,
- autovalidate: _autoValidate.value,
- helpText: widget.helpText,
- errorInvalidRangeText: widget.errorInvalidRangeText,
- errorFormatText: widget.errorFormatText,
- errorInvalidText: widget.errorInvalidText,
- fieldStartHintText: widget.fieldStartHintText,
- fieldEndHintText: widget.fieldEndHintText,
- fieldStartLabelText: widget.fieldStartLabelText,
- fieldEndLabelText: widget.fieldEndLabelText,
- ),
- const Spacer(),
- ],
- ),
- ),
- onConfirm: _handleOk,
- onCancel: _handleCancel,
- entryModeButton: showEntryModeButton
- ? CustomIconButton(
- icon: Icon(
- Icons.calendar_today,
- size: 24.s,
- ),
- padding: EdgeInsets.zero,
- constraints: const BoxConstraints(),
- color: onPrimarySurface,
- tooltip: localizations.calendarModeButtonLabel,
- onPressed: _handleEntryModeToggle,
- )
- : null,
- confirmText: widget.confirmText ?? localizations.okButtonLabel,
- cancelText: widget.cancelText ?? localizations.cancelButtonLabel,
- helpText: widget.helpText ?? localizations.dateRangePickerHelpText,
- );
- final DialogTheme dialogTheme = Theme.of(context).dialogTheme;
- size = orientation == Orientation.portrait
- ? _inputPortraitDialogSize
- : _inputRangeLandscapeDialogSize;
- insetPadding = EdgeInsets.symmetric(horizontal: 16.s, vertical: 24.s);
- shape = dialogTheme.shape;
- elevation = dialogTheme.elevation ?? 24;
- break;
- }
- return CustomDialog(
- insetPadding: insetPadding,
- shape: shape,
- elevation: elevation,
- clipBehavior: Clip.antiAlias,
- child: AnimatedContainer(
- width: size.width,
- height: size.height,
- duration: _dialogSizeAnimationDuration,
- curve: Curves.easeIn,
- child: MediaQuery(
- data: MediaQuery.of(context).copyWith(
- textScaleFactor: textScaleFactor,
- ),
- child: Builder(builder: (BuildContext context) {
- return contents;
- }),
- ),
- ),
- );
- }
- }
- class _CalendarRangePickerDialog extends StatelessWidget {
- const _CalendarRangePickerDialog({
- super.key,
- required this.selectedStartDate,
- required this.selectedEndDate,
- required this.firstDate,
- required this.lastDate,
- required this.currentDate,
- required this.onStartDateChanged,
- required this.onEndDateChanged,
- required this.onConfirm,
- required this.onCancel,
- required this.confirmText,
- required this.helpText,
- this.entryModeButton,
- });
- final DateTime? selectedStartDate;
- final DateTime? selectedEndDate;
- final DateTime firstDate;
- final DateTime lastDate;
- final DateTime? currentDate;
- final ValueChanged<DateTime> onStartDateChanged;
- final ValueChanged<DateTime?> onEndDateChanged;
- final VoidCallback? onConfirm;
- final VoidCallback? onCancel;
- final String confirmText;
- final String helpText;
- final Widget? entryModeButton;
- @override
- Widget build(BuildContext context) {
- final ThemeData theme = Theme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final Orientation orientation = MediaQuery.of(context).orientation;
- final TextTheme textTheme = theme.textTheme;
- final Color headerForeground = colorScheme.brightness == Brightness.light
- ? colorScheme.onPrimary
- : colorScheme.onSurface;
- final Color headerDisabledForeground = headerForeground.withOpacity(0.38);
- final String startDateText = _formatRangeStartDate(
- localizations, selectedStartDate, selectedEndDate);
- final String endDateText = _formatRangeEndDate(
- localizations, selectedStartDate, selectedEndDate, DateTime.now());
- final TextStyle? headlineStyle = textTheme.headlineSmall;
- final TextStyle? startDateStyle = headlineStyle?.apply(
- color: selectedStartDate != null
- ? headerForeground
- : headerDisabledForeground,
- );
- final TextStyle? endDateStyle = headlineStyle?.apply(
- color:
- selectedEndDate != null ? headerForeground : headerDisabledForeground,
- );
- final TextStyle saveButtonStyle = textTheme.labelLarge!.apply(
- color: onConfirm != null ? headerForeground : headerDisabledForeground,
- );
- return SafeArea(
- top: false,
- left: false,
- right: false,
- child: Scaffold(
- appBar: AppBar(
- leading: CloseButton(
- onPressed: onCancel,
- ),
- actions: <Widget>[
- if (orientation == Orientation.landscape && entryModeButton != null)
- entryModeButton!,
- CustomTextButton(
- onPressed: onConfirm,
- child: Text(confirmText, style: saveButtonStyle),
- ),
- SizedBox(width: 8.s),
- ],
- bottom: PreferredSize(
- preferredSize: Size(double.infinity, 64.s),
- child: Row(children: <Widget>[
- SizedBox(
- width:
- MediaQuery.of(context).size.width < 360.s ? 42.s : 72.s),
- Expanded(
- child: Semantics(
- label: '$helpText $startDateText to $endDateText',
- excludeSemantics: true,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: <Widget>[
- Text(
- helpText,
- style: textTheme.labelSmall!.apply(
- color: headerForeground,
- ),
- ),
- SizedBox(height: 8.s),
- Row(
- children: <Widget>[
- Text(
- startDateText,
- style: startDateStyle,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- Text(
- ' – ',
- style: startDateStyle,
- ),
- Flexible(
- child: Text(
- endDateText,
- style: endDateStyle,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ],
- ),
- SizedBox(height: 16.s),
- ],
- ),
- ),
- ),
- if (orientation == Orientation.portrait &&
- entryModeButton != null)
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 8.s),
- child: entryModeButton,
- ),
- ]),
- ),
- ),
- body: _CalendarDateRangePicker(
- initialStartDate: selectedStartDate,
- initialEndDate: selectedEndDate,
- firstDate: firstDate,
- lastDate: lastDate,
- currentDate: currentDate,
- onStartDateChanged: onStartDateChanged,
- onEndDateChanged: onEndDateChanged,
- ),
- ),
- );
- }
- }
- const Duration _monthScrollDuration = Duration(milliseconds: 200);
- double get _monthItemHeaderHeight => 58.s;
- double get _monthItemFooterHeight => 12.s;
- double get _monthItemRowHeight => 42.s;
- double get _monthItemSpaceBetweenRows => 8.s;
- double get _horizontalPadding => 8.s;
- double get _maxCalendarWidthLandscape => 384.s;
- double get _maxCalendarWidthPortrait => 480.s;
- /// Displays a scrollable calendar grid that allows a user to select a range
- /// of dates.
- class _CalendarDateRangePicker extends StatefulWidget {
- /// Creates a scrollable calendar grid for picking date ranges.
- _CalendarDateRangePicker({
- DateTime? initialStartDate,
- DateTime? initialEndDate,
- required DateTime firstDate,
- required DateTime lastDate,
- DateTime? currentDate,
- required this.onStartDateChanged,
- required this.onEndDateChanged,
- }) : initialStartDate = initialStartDate != null
- ? DateUtils.dateOnly(initialStartDate)
- : null,
- initialEndDate =
- initialEndDate != null ? DateUtils.dateOnly(initialEndDate) : null,
- assert(firstDate != null),
- assert(lastDate != null),
- firstDate = DateUtils.dateOnly(firstDate),
- lastDate = DateUtils.dateOnly(lastDate),
- currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) {
- assert(
- this.initialStartDate == null ||
- this.initialEndDate == null ||
- !this.initialStartDate!.isAfter(initialEndDate!),
- 'initialStartDate must be on or before initialEndDate.',
- );
- assert(
- !this.lastDate.isBefore(this.firstDate),
- 'firstDate must be on or before lastDate.',
- );
- }
- /// The [DateTime] that represents the start of the initial date range selection.
- final DateTime? initialStartDate;
- /// The [DateTime] that represents the end of the initial date range selection.
- final DateTime? initialEndDate;
- /// The earliest allowable [DateTime] that the user can select.
- final DateTime firstDate;
- /// The latest allowable [DateTime] that the user can select.
- final DateTime lastDate;
- /// The [DateTime] representing today. It will be highlighted in the day grid.
- final DateTime currentDate;
- /// Called when the user changes the start date of the selected range.
- final ValueChanged<DateTime>? onStartDateChanged;
- /// Called when the user changes the end date of the selected range.
- final ValueChanged<DateTime?>? onEndDateChanged;
- @override
- _CalendarDateRangePickerState createState() =>
- _CalendarDateRangePickerState();
- }
- class _CalendarDateRangePickerState extends State<_CalendarDateRangePicker> {
- final GlobalKey _scrollViewKey = GlobalKey();
- DateTime? _startDate;
- DateTime? _endDate;
- int _initialMonthIndex = 0;
- late ScrollController _controller;
- late bool _showWeekBottomDivider;
- @override
- void initState() {
- super.initState();
- _controller = ScrollController();
- _controller.addListener(_scrollListener);
- _startDate = widget.initialStartDate;
- _endDate = widget.initialEndDate;
- // Calculate the index for the initially displayed month. This is needed to
- // divide the list of months into two `SliverList`s.
- final DateTime initialDate = widget.initialStartDate ?? widget.currentDate;
- if (!initialDate.isBefore(widget.firstDate) &&
- !initialDate.isAfter(widget.lastDate)) {
- _initialMonthIndex = DateUtils.monthDelta(widget.firstDate, initialDate);
- }
- _showWeekBottomDivider = _initialMonthIndex != 0;
- }
- @override
- void dispose() {
- _controller.dispose();
- super.dispose();
- }
- void _scrollListener() {
- if (_controller.offset <= _controller.position.minScrollExtent) {
- setState(() {
- _showWeekBottomDivider = false;
- });
- } else if (!_showWeekBottomDivider) {
- setState(() {
- _showWeekBottomDivider = true;
- });
- }
- }
- int get _numberOfMonths =>
- DateUtils.monthDelta(widget.firstDate, widget.lastDate) + 1;
- void _vibrate() {
- switch (Theme.of(context).platform) {
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- HapticFeedback.vibrate();
- break;
- case TargetPlatform.iOS:
- case TargetPlatform.linux:
- case TargetPlatform.macOS:
- case TargetPlatform.windows:
- break;
- }
- }
- // This updates the selected date range using this logic:
- //
- // * From the unselected state, selecting one date creates the start date.
- // * If the next selection is before the start date, reset date range and
- // set the start date to that selection.
- // * If the next selection is on or after the start date, set the end date
- // to that selection.
- // * After both start and end dates are selected, any subsequent selection
- // resets the date range and sets start date to that selection.
- void _updateSelection(DateTime date) {
- _vibrate();
- setState(() {
- if (_startDate != null &&
- _endDate == null &&
- !date.isBefore(_startDate!)) {
- _endDate = date;
- widget.onEndDateChanged?.call(_endDate);
- } else {
- _startDate = date;
- widget.onStartDateChanged?.call(_startDate!);
- if (_endDate != null) {
- _endDate = null;
- widget.onEndDateChanged?.call(_endDate);
- }
- }
- });
- }
- Widget _buildMonthItem(
- BuildContext context, int index, bool beforeInitialMonth) {
- final int monthIndex = beforeInitialMonth
- ? _initialMonthIndex - index - 1
- : _initialMonthIndex + index;
- final DateTime month =
- DateUtils.addMonthsToMonthDate(widget.firstDate, monthIndex);
- return _MonthItem(
- selectedDateStart: _startDate,
- selectedDateEnd: _endDate,
- currentDate: widget.currentDate,
- firstDate: widget.firstDate,
- lastDate: widget.lastDate,
- displayedMonth: month,
- onChanged: _updateSelection,
- );
- }
- @override
- Widget build(BuildContext context) {
- const Key sliverAfterKey = Key('sliverAfterKey');
- return Column(
- children: <Widget>[
- const _DayHeaders(),
- if (_showWeekBottomDivider) const Divider(height: 0),
- Expanded(
- child: _CalendarKeyboardNavigator(
- firstDate: widget.firstDate,
- lastDate: widget.lastDate,
- initialFocusedDay:
- _startDate ?? widget.initialStartDate ?? widget.currentDate,
- // In order to prevent performance issues when displaying the
- // correct initial month, 2 `SliverList`s are used to split the
- // months. The first item in the second SliverList is the initial
- // month to be displayed.
- child: CustomScrollView(
- key: _scrollViewKey,
- controller: _controller,
- center: sliverAfterKey,
- slivers: <Widget>[
- SliverList(
- delegate: SliverChildBuilderDelegate(
- (BuildContext context, int index) =>
- _buildMonthItem(context, index, true),
- childCount: _initialMonthIndex,
- ),
- ),
- SliverList(
- key: sliverAfterKey,
- delegate: SliverChildBuilderDelegate(
- (BuildContext context, int index) =>
- _buildMonthItem(context, index, false),
- childCount: _numberOfMonths - _initialMonthIndex,
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- );
- }
- }
- class _CalendarKeyboardNavigator extends StatefulWidget {
- const _CalendarKeyboardNavigator({
- required this.child,
- required this.firstDate,
- required this.lastDate,
- required this.initialFocusedDay,
- });
- final Widget child;
- final DateTime firstDate;
- final DateTime lastDate;
- final DateTime initialFocusedDay;
- @override
- _CalendarKeyboardNavigatorState createState() =>
- _CalendarKeyboardNavigatorState();
- }
- class _CalendarKeyboardNavigatorState
- extends State<_CalendarKeyboardNavigator> {
- final Map<ShortcutActivator, Intent> _shortcutMap =
- const <ShortcutActivator, Intent>{
- SingleActivator(LogicalKeyboardKey.arrowLeft):
- DirectionalFocusIntent(TraversalDirection.left),
- SingleActivator(LogicalKeyboardKey.arrowRight):
- DirectionalFocusIntent(TraversalDirection.right),
- SingleActivator(LogicalKeyboardKey.arrowDown):
- DirectionalFocusIntent(TraversalDirection.down),
- SingleActivator(LogicalKeyboardKey.arrowUp):
- DirectionalFocusIntent(TraversalDirection.up),
- };
- late Map<Type, Action<Intent>> _actionMap;
- late FocusNode _dayGridFocus;
- TraversalDirection? _dayTraversalDirection;
- DateTime? _focusedDay;
- @override
- void initState() {
- super.initState();
- _actionMap = <Type, Action<Intent>>{
- NextFocusIntent:
- CallbackAction<NextFocusIntent>(onInvoke: _handleGridNextFocus),
- PreviousFocusIntent: CallbackAction<PreviousFocusIntent>(
- onInvoke: _handleGridPreviousFocus),
- DirectionalFocusIntent: CallbackAction<DirectionalFocusIntent>(
- onInvoke: _handleDirectionFocus),
- };
- _dayGridFocus = FocusNode(debugLabel: 'Day Grid');
- }
- @override
- void dispose() {
- _dayGridFocus.dispose();
- super.dispose();
- }
- void _handleGridFocusChange(bool focused) {
- setState(() {
- if (focused) {
- _focusedDay ??= widget.initialFocusedDay;
- }
- });
- }
- /// Move focus to the next element after the day grid.
- void _handleGridNextFocus(NextFocusIntent intent) {
- _dayGridFocus.requestFocus();
- _dayGridFocus.nextFocus();
- }
- /// Move focus to the previous element before the day grid.
- void _handleGridPreviousFocus(PreviousFocusIntent intent) {
- _dayGridFocus.requestFocus();
- _dayGridFocus.previousFocus();
- }
- /// Move the internal focus date in the direction of the given intent.
- ///
- /// This will attempt to move the focused day to the next selectable day in
- /// the given direction. If the new date is not in the current month, then
- /// the page view will be scrolled to show the new date's month.
- ///
- /// For horizontal directions, it will move forward or backward a day (depending
- /// on the current [TextDirection]). For vertical directions it will move up and
- /// down a week at a time.
- void _handleDirectionFocus(DirectionalFocusIntent intent) {
- assert(_focusedDay != null);
- setState(() {
- final DateTime? nextDate =
- _nextDateInDirection(_focusedDay!, intent.direction);
- if (nextDate != null) {
- _focusedDay = nextDate;
- _dayTraversalDirection = intent.direction;
- }
- });
- }
- static const Map<TraversalDirection, int> _directionOffset =
- <TraversalDirection, int>{
- TraversalDirection.up: -DateTime.daysPerWeek,
- TraversalDirection.right: 1,
- TraversalDirection.down: DateTime.daysPerWeek,
- TraversalDirection.left: -1,
- };
- int _dayDirectionOffset(
- TraversalDirection traversalDirection, TextDirection textDirection) {
- // Swap left and right if the text direction if RTL
- if (textDirection == TextDirection.rtl) {
- if (traversalDirection == TraversalDirection.left) {
- traversalDirection = TraversalDirection.right;
- } else if (traversalDirection == TraversalDirection.right) {
- traversalDirection = TraversalDirection.left;
- }
- }
- return _directionOffset[traversalDirection]!;
- }
- DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
- final TextDirection textDirection = Directionality.of(context);
- final DateTime nextDate = DateUtils.addDaysToDate(
- date, _dayDirectionOffset(direction, textDirection));
- if (!nextDate.isBefore(widget.firstDate) &&
- !nextDate.isAfter(widget.lastDate)) {
- return nextDate;
- }
- return null;
- }
- @override
- Widget build(BuildContext context) {
- return FocusableActionDetector(
- shortcuts: _shortcutMap,
- actions: _actionMap,
- focusNode: _dayGridFocus,
- onFocusChange: _handleGridFocusChange,
- child: _FocusedDate(
- date: _dayGridFocus.hasFocus ? _focusedDay : null,
- scrollDirection: _dayGridFocus.hasFocus ? _dayTraversalDirection : null,
- child: widget.child,
- ),
- );
- }
- }
- /// InheritedWidget indicating what the current focused date is for its children.
- ///
- /// This is used by the [_MonthPicker] to let its children [_DayPicker]s know
- /// what the currently focused date (if any) should be.
- class _FocusedDate extends InheritedWidget {
- const _FocusedDate({
- required super.child,
- this.date,
- this.scrollDirection,
- });
- final DateTime? date;
- final TraversalDirection? scrollDirection;
- @override
- bool updateShouldNotify(_FocusedDate oldWidget) {
- return !DateUtils.isSameDay(date, oldWidget.date) ||
- scrollDirection != oldWidget.scrollDirection;
- }
- static _FocusedDate? of(BuildContext context) {
- return context.dependOnInheritedWidgetOfExactType<_FocusedDate>();
- }
- }
- class _DayHeaders extends StatelessWidget {
- const _DayHeaders();
- /// 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;
- }
- @override
- Widget build(BuildContext context) {
- final ThemeData themeData = Theme.of(context);
- final ColorScheme colorScheme = themeData.colorScheme;
- final TextStyle textStyle =
- themeData.textTheme.titleSmall!.apply(color: colorScheme.onSurface);
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final List<Widget> labels = _getDayHeaders(textStyle, localizations);
- // Add leading and trailing containers for edges of the custom grid layout.
- labels.insert(0, Container());
- labels.add(Container());
- return Container(
- constraints: BoxConstraints(
- maxWidth: MediaQuery.of(context).orientation == Orientation.landscape
- ? _maxCalendarWidthLandscape
- : _maxCalendarWidthPortrait,
- maxHeight: _monthItemRowHeight,
- ),
- child: GridView.custom(
- shrinkWrap: true,
- gridDelegate: _monthItemGridDelegate,
- childrenDelegate: SliverChildListDelegate(
- labels,
- addRepaintBoundaries: false,
- ),
- ),
- );
- }
- }
- class _MonthItemGridDelegate extends SliverGridDelegate {
- const _MonthItemGridDelegate();
- @override
- SliverGridLayout getLayout(SliverConstraints constraints) {
- final double tileWidth =
- (constraints.crossAxisExtent - 2 * _horizontalPadding) /
- DateTime.daysPerWeek;
- return _MonthSliverGridLayout(
- crossAxisCount: DateTime.daysPerWeek + 2,
- dayChildWidth: tileWidth,
- edgeChildWidth: _horizontalPadding,
- reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
- );
- }
- @override
- bool shouldRelayout(_MonthItemGridDelegate oldDelegate) => false;
- }
- const _MonthItemGridDelegate _monthItemGridDelegate = _MonthItemGridDelegate();
- class _MonthSliverGridLayout extends SliverGridLayout {
- /// Creates a layout that uses equally sized and spaced tiles for each day of
- /// the week and an additional edge tile for padding at the start and end of
- /// each row.
- ///
- /// This is necessary to facilitate the painting of the range highlight
- /// correctly.
- const _MonthSliverGridLayout({
- required this.crossAxisCount,
- required this.dayChildWidth,
- required this.edgeChildWidth,
- required this.reverseCrossAxis,
- }) : assert(crossAxisCount != null && crossAxisCount > 0),
- assert(dayChildWidth != null && dayChildWidth >= 0),
- assert(edgeChildWidth != null && edgeChildWidth >= 0),
- assert(reverseCrossAxis != null);
- /// The number of children in the cross axis.
- final int crossAxisCount;
- /// The width in logical pixels of the day child widgets.
- final double dayChildWidth;
- /// The width in logical pixels of the edge child widgets.
- final double edgeChildWidth;
- /// Whether the children should be placed in the opposite order of increasing
- /// coordinates in the cross axis.
- ///
- /// For example, if the cross axis is horizontal, the children are placed from
- /// left to right when [reverseCrossAxis] is false and from right to left when
- /// [reverseCrossAxis] is true.
- ///
- /// Typically set to the return value of [axisDirectionIsReversed] applied to
- /// the [SliverConstraints.crossAxisDirection].
- final bool reverseCrossAxis;
- /// The number of logical pixels from the leading edge of one row to the
- /// leading edge of the next row.
- double get _rowHeight {
- return _monthItemRowHeight + _monthItemSpaceBetweenRows;
- }
- /// The height in logical pixels of the children widgets.
- double get _childHeight {
- return _monthItemRowHeight;
- }
- @override
- int getMinChildIndexForScrollOffset(double scrollOffset) {
- return crossAxisCount * (scrollOffset ~/ _rowHeight);
- }
- @override
- int getMaxChildIndexForScrollOffset(double scrollOffset) {
- final int mainAxisCount = (scrollOffset / _rowHeight).ceil();
- return math.max(0, crossAxisCount * mainAxisCount - 1);
- }
- double _getCrossAxisOffset(double crossAxisStart, bool isPadding) {
- if (reverseCrossAxis) {
- return ((crossAxisCount - 2) * dayChildWidth + 2 * edgeChildWidth) -
- crossAxisStart -
- (isPadding ? edgeChildWidth : dayChildWidth);
- }
- return crossAxisStart;
- }
- @override
- SliverGridGeometry getGeometryForChildIndex(int index) {
- final int adjustedIndex = index % crossAxisCount;
- final bool isEdge =
- adjustedIndex == 0 || adjustedIndex == crossAxisCount - 1;
- final double crossAxisStart =
- math.max(0, (adjustedIndex - 1) * dayChildWidth + edgeChildWidth);
- return SliverGridGeometry(
- scrollOffset: (index ~/ crossAxisCount) * _rowHeight,
- crossAxisOffset: _getCrossAxisOffset(crossAxisStart, isEdge),
- mainAxisExtent: _childHeight,
- crossAxisExtent: isEdge ? edgeChildWidth : dayChildWidth,
- );
- }
- @override
- double computeMaxScrollOffset(int childCount) {
- assert(childCount >= 0);
- final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1;
- final double mainAxisSpacing = _rowHeight - _childHeight;
- return _rowHeight * mainAxisCount - mainAxisSpacing;
- }
- }
- /// Displays the days of a given month and allows choosing a date range.
- ///
- /// The days are arranged in a rectangular grid with one column for each day of
- /// the week.
- class _MonthItem extends StatefulWidget {
- /// Creates a month item.
- _MonthItem({
- required this.selectedDateStart,
- required this.selectedDateEnd,
- required this.currentDate,
- required this.onChanged,
- required this.firstDate,
- required this.lastDate,
- required this.displayedMonth,
- this.dragStartBehavior = DragStartBehavior.start,
- }) : assert(firstDate != null),
- assert(lastDate != null),
- assert(!firstDate.isAfter(lastDate)),
- assert(selectedDateStart == null ||
- !selectedDateStart.isBefore(firstDate)),
- assert(selectedDateEnd == null || !selectedDateEnd.isBefore(firstDate)),
- assert(
- selectedDateStart == null || !selectedDateStart.isAfter(lastDate)),
- assert(selectedDateEnd == null || !selectedDateEnd.isAfter(lastDate)),
- assert(selectedDateStart == null ||
- selectedDateEnd == null ||
- !selectedDateStart.isAfter(selectedDateEnd)),
- assert(currentDate != null),
- assert(onChanged != null),
- assert(displayedMonth != null),
- assert(dragStartBehavior != null);
- /// The currently selected start date.
- ///
- /// This date is highlighted in the picker.
- final DateTime? selectedDateStart;
- /// The currently selected end date.
- ///
- /// This date is highlighted in the picker.
- final DateTime? selectedDateEnd;
- /// 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;
- /// 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 at the position where the drag gesture won
- /// the arena. If set to [DragStartBehavior.down] it will begin at the position
- /// where 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;
- @override
- _MonthItemState createState() => _MonthItemState();
- }
- class _MonthItemState extends State<_MonthItem> {
- /// List of [FocusNode]s, one for each day of the month.
- late List<FocusNode> _dayFocusNodes;
- @override
- void initState() {
- super.initState();
- final int daysInMonth = DateUtils.getDaysInMonth(
- widget.displayedMonth.year, widget.displayedMonth.month);
- _dayFocusNodes = List<FocusNode>.generate(
- daysInMonth,
- (int index) =>
- FocusNode(skipTraversal: true, debugLabel: 'Day ${index + 1}'),
- );
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- // Check to see if the focused date is in this month, if so focus it.
- final DateTime? focusedDate = _FocusedDate.of(context)?.date;
- if (focusedDate != null &&
- DateUtils.isSameMonth(widget.displayedMonth, focusedDate)) {
- _dayFocusNodes[focusedDate.day - 1].requestFocus();
- }
- }
- @override
- void dispose() {
- for (final FocusNode node in _dayFocusNodes) {
- node.dispose();
- }
- super.dispose();
- }
- Color _highlightColor(BuildContext context) {
- return Theme.of(context).colorScheme.primary.withOpacity(0.12);
- }
- void _dayFocusChanged(bool focused) {
- if (focused) {
- final TraversalDirection? focusDirection =
- _FocusedDate.of(context)?.scrollDirection;
- if (focusDirection != null) {
- ScrollPositionAlignmentPolicy policy =
- ScrollPositionAlignmentPolicy.explicit;
- switch (focusDirection) {
- case TraversalDirection.up:
- case TraversalDirection.left:
- policy = ScrollPositionAlignmentPolicy.keepVisibleAtStart;
- break;
- case TraversalDirection.right:
- case TraversalDirection.down:
- policy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd;
- break;
- }
- Scrollable.ensureVisible(
- primaryFocus!.context!,
- duration: _monthScrollDuration,
- alignmentPolicy: policy,
- );
- }
- }
- }
- Widget _buildDayItem(BuildContext context, DateTime dayToBuild,
- int firstDayOffset, int daysInMonth) {
- final ThemeData theme = Theme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
- final TextTheme textTheme = theme.textTheme;
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final TextDirection textDirection = Directionality.of(context);
- final Color highlightColor = _highlightColor(context);
- final int day = dayToBuild.day;
- final bool isDisabled = dayToBuild.isAfter(widget.lastDate) ||
- dayToBuild.isBefore(widget.firstDate);
- BoxDecoration? decoration;
- TextStyle? itemStyle = textTheme.bodyMedium;
- final bool isRangeSelected =
- widget.selectedDateStart != null && widget.selectedDateEnd != null;
- final bool isSelectedDayStart = widget.selectedDateStart != null &&
- dayToBuild.isAtSameMomentAs(widget.selectedDateStart!);
- final bool isSelectedDayEnd = widget.selectedDateEnd != null &&
- dayToBuild.isAtSameMomentAs(widget.selectedDateEnd!);
- final bool isInRange = isRangeSelected &&
- dayToBuild.isAfter(widget.selectedDateStart!) &&
- dayToBuild.isBefore(widget.selectedDateEnd!);
- _HighlightPainter? highlightPainter;
- if (isSelectedDayStart || isSelectedDayEnd) {
- // The selected start and end dates gets a circle background
- // highlight, and a contrasting text color.
- itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.onPrimary);
- decoration = BoxDecoration(
- color: colorScheme.primary,
- shape: BoxShape.circle,
- );
- if (isRangeSelected &&
- widget.selectedDateStart != widget.selectedDateEnd) {
- final _HighlightPainterStyle style = isSelectedDayStart
- ? _HighlightPainterStyle.highlightTrailing
- : _HighlightPainterStyle.highlightLeading;
- highlightPainter = _HighlightPainter(
- color: highlightColor,
- style: style,
- textDirection: textDirection,
- );
- }
- } else if (isInRange) {
- // The days within the range get a light background highlight.
- highlightPainter = _HighlightPainter(
- color: highlightColor,
- style: _HighlightPainterStyle.highlightAll,
- textDirection: textDirection,
- );
- } else if (isDisabled) {
- itemStyle = textTheme.bodyMedium
- ?.apply(color: colorScheme.onSurface.withOpacity(0.38));
- } else if (DateUtils.isSameDay(widget.currentDate, dayToBuild)) {
- // The current day gets a different text color and a circle stroke
- // border.
- itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.primary);
- decoration = BoxDecoration(
- border: Border.all(color: colorScheme.primary, width: 1.s),
- shape: BoxShape.circle,
- );
- }
- // 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.
- String semanticLabel =
- '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}';
- if (isSelectedDayStart) {
- semanticLabel =
- localizations.dateRangeStartDateSemanticLabel(semanticLabel);
- } else if (isSelectedDayEnd) {
- semanticLabel =
- localizations.dateRangeEndDateSemanticLabel(semanticLabel);
- }
- Widget dayWidget = Container(
- decoration: decoration,
- child: Center(
- child: Semantics(
- label: semanticLabel,
- selected: isSelectedDayStart || isSelectedDayEnd,
- child: ExcludeSemantics(
- child: Text(localizations.formatDecimal(day), style: itemStyle),
- ),
- ),
- ),
- );
- if (highlightPainter != null) {
- dayWidget = CustomPaint(
- painter: highlightPainter,
- child: dayWidget,
- );
- }
- if (!isDisabled) {
- dayWidget = InkResponse(
- focusNode: _dayFocusNodes[day - 1],
- onTap: () => widget.onChanged(dayToBuild),
- radius: _monthItemRowHeight / 2 + 4.s,
- splashColor: colorScheme.primary.withOpacity(0.38),
- onFocusChange: _dayFocusChanged,
- child: dayWidget,
- );
- }
- return dayWidget;
- }
- Widget _buildEdgeContainer(BuildContext context, bool isHighlighted) {
- return Container(color: isHighlighted ? _highlightColor(context) : null);
- }
- @override
- Widget build(BuildContext context) {
- final ThemeData themeData = Theme.of(context);
- final TextTheme textTheme = themeData.textTheme;
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final int year = widget.displayedMonth.year;
- final int month = widget.displayedMonth.month;
- final int daysInMonth = DateUtils.getDaysInMonth(year, month);
- final int dayOffset = DateUtils.firstDayOffset(year, month, localizations);
- final int weeks = ((daysInMonth + dayOffset) / DateTime.daysPerWeek).ceil();
- final double gridHeight =
- weeks * _monthItemRowHeight + (weeks - 1) * _monthItemSpaceBetweenRows;
- final List<Widget> dayItems = <Widget>[];
- 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 - dayOffset + 1;
- if (day > daysInMonth) {
- break;
- }
- if (day < 1) {
- dayItems.add(Container());
- } else {
- final DateTime dayToBuild = DateTime(year, month, day);
- final Widget dayItem = _buildDayItem(
- context,
- dayToBuild,
- dayOffset,
- daysInMonth,
- );
- dayItems.add(dayItem);
- }
- }
- // Add the leading/trailing edge containers to each week in order to
- // correctly extend the range highlight.
- final List<Widget> paddedDayItems = <Widget>[];
- for (int i = 0; i < weeks; i++) {
- final int start = i * DateTime.daysPerWeek;
- final int end = math.min(
- start + DateTime.daysPerWeek,
- dayItems.length,
- );
- final List<Widget> weekList = dayItems.sublist(start, end);
- final DateTime dateAfterLeadingPadding =
- DateTime(year, month, start - dayOffset + 1);
- // Only color the edge container if it is after the start date and
- // on/before the end date.
- final bool isLeadingInRange = !(dayOffset > 0 && i == 0) &&
- widget.selectedDateStart != null &&
- widget.selectedDateEnd != null &&
- dateAfterLeadingPadding.isAfter(widget.selectedDateStart!) &&
- !dateAfterLeadingPadding.isAfter(widget.selectedDateEnd!);
- weekList.insert(0, _buildEdgeContainer(context, isLeadingInRange));
- // Only add a trailing edge container if it is for a full week and not a
- // partial week.
- if (end < dayItems.length ||
- (end == dayItems.length &&
- dayItems.length % DateTime.daysPerWeek == 0)) {
- final DateTime dateBeforeTrailingPadding =
- DateTime(year, month, end - dayOffset);
- // Only color the edge container if it is on/after the start date and
- // before the end date.
- final bool isTrailingInRange = widget.selectedDateStart != null &&
- widget.selectedDateEnd != null &&
- !dateBeforeTrailingPadding.isBefore(widget.selectedDateStart!) &&
- dateBeforeTrailingPadding.isBefore(widget.selectedDateEnd!);
- weekList.add(_buildEdgeContainer(context, isTrailingInRange));
- }
- paddedDayItems.addAll(weekList);
- }
- final double maxWidth =
- MediaQuery.of(context).orientation == Orientation.landscape
- ? _maxCalendarWidthLandscape
- : _maxCalendarWidthPortrait;
- return Column(
- children: <Widget>[
- Container(
- constraints: BoxConstraints(maxWidth: maxWidth),
- height: _monthItemHeaderHeight,
- padding: EdgeInsets.symmetric(horizontal: 16.s),
- alignment: AlignmentDirectional.centerStart,
- child: ExcludeSemantics(
- child: Text(
- localizations.formatMonthYear(widget.displayedMonth),
- style: textTheme.bodyMedium!
- .apply(color: themeData.colorScheme.onSurface),
- ),
- ),
- ),
- Container(
- constraints: BoxConstraints(
- maxWidth: maxWidth,
- maxHeight: gridHeight,
- ),
- child: GridView.custom(
- physics: const NeverScrollableScrollPhysics(),
- gridDelegate: _monthItemGridDelegate,
- childrenDelegate: SliverChildListDelegate(
- paddedDayItems,
- addRepaintBoundaries: false,
- ),
- ),
- ),
- SizedBox(height: _monthItemFooterHeight),
- ],
- );
- }
- }
- /// Determines which style to use to paint the highlight.
- enum _HighlightPainterStyle {
- /// Paints nothing.
- none,
- /// Paints a rectangle that occupies the leading half of the space.
- highlightLeading,
- /// Paints a rectangle that occupies the trailing half of the space.
- highlightTrailing,
- /// Paints a rectangle that occupies all available space.
- highlightAll,
- }
- /// This custom painter will add a background highlight to its child.
- ///
- /// This highlight will be drawn depending on the [style], [color], and
- /// [textDirection] supplied. It will either paint a rectangle on the
- /// left/right, a full rectangle, or nothing at all. This logic is determined by
- /// a combination of the [style] and [textDirection].
- class _HighlightPainter extends CustomPainter {
- _HighlightPainter({
- required this.color,
- this.style = _HighlightPainterStyle.none,
- this.textDirection,
- });
- final Color color;
- final _HighlightPainterStyle style;
- final TextDirection? textDirection;
- @override
- void paint(Canvas canvas, Size size) {
- if (style == _HighlightPainterStyle.none) {
- return;
- }
- final Paint paint = Paint()
- ..color = color
- ..style = PaintingStyle.fill;
- final Rect rectLeft = Rect.fromLTWH(0, 0, size.width / 2, size.height);
- final Rect rectRight =
- Rect.fromLTWH(size.width / 2, 0, size.width / 2, size.height);
- switch (style) {
- case _HighlightPainterStyle.highlightTrailing:
- canvas.drawRect(
- textDirection == TextDirection.ltr ? rectRight : rectLeft,
- paint,
- );
- break;
- case _HighlightPainterStyle.highlightLeading:
- canvas.drawRect(
- textDirection == TextDirection.ltr ? rectLeft : rectRight,
- paint,
- );
- break;
- case _HighlightPainterStyle.highlightAll:
- canvas.drawRect(
- Rect.fromLTWH(0, 0, size.width, size.height),
- paint,
- );
- break;
- case _HighlightPainterStyle.none:
- break;
- }
- }
- @override
- bool shouldRepaint(CustomPainter oldDelegate) => false;
- }
- class _InputDateRangePickerDialog extends StatelessWidget {
- const _InputDateRangePickerDialog({
- required this.selectedStartDate,
- required this.selectedEndDate,
- required this.currentDate,
- required this.picker,
- required this.onConfirm,
- required this.onCancel,
- required this.confirmText,
- required this.cancelText,
- required this.helpText,
- required this.entryModeButton,
- });
- final DateTime? selectedStartDate;
- final DateTime? selectedEndDate;
- final DateTime? currentDate;
- final Widget picker;
- final VoidCallback onConfirm;
- final VoidCallback onCancel;
- final String? confirmText;
- final String? cancelText;
- final String? helpText;
- final Widget? entryModeButton;
- String _formatDateRange(
- BuildContext context, DateTime? start, DateTime? end, DateTime now) {
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final String startText = _formatRangeStartDate(localizations, start, end);
- final String endText = _formatRangeEndDate(localizations, start, end, now);
- if (start == null || end == null) {
- return localizations.unspecifiedDateRange;
- }
- if (Directionality.of(context) == TextDirection.ltr) {
- return '$startText – $endText';
- } else {
- return '$endText – $startText';
- }
- }
- @override
- Widget build(BuildContext context) {
- final ThemeData theme = Theme.of(context);
- final ColorScheme colorScheme = theme.colorScheme;
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final Orientation orientation = MediaQuery.of(context).orientation;
- final TextTheme textTheme = theme.textTheme;
- final Color onPrimarySurfaceColor =
- colorScheme.brightness == Brightness.light
- ? colorScheme.onPrimary
- : colorScheme.onSurface;
- final TextStyle? dateStyle = orientation == Orientation.landscape
- ? textTheme.headlineSmall?.apply(color: onPrimarySurfaceColor)
- : textTheme.headlineMedium?.apply(color: onPrimarySurfaceColor);
- final String dateText = _formatDateRange(
- context, selectedStartDate, selectedEndDate, currentDate!);
- final String semanticDateText = selectedStartDate != null &&
- selectedEndDate != null
- ? '${localizations.formatMediumDate(selectedStartDate!)} – ${localizations.formatMediumDate(selectedEndDate!)}'
- : '';
- final Widget header = _DatePickerHeader(
- helpText: helpText ?? localizations.dateRangePickerHelpText,
- titleText: dateText,
- titleSemanticsLabel: semanticDateText,
- titleStyle: dateStyle,
- orientation: orientation,
- isShort: orientation == Orientation.landscape,
- entryModeButton: entryModeButton,
- );
- final Widget actions = Container(
- alignment: AlignmentDirectional.centerEnd,
- constraints: BoxConstraints(minHeight: 52.s),
- padding: EdgeInsets.symmetric(horizontal: 8.s),
- child: OverflowBar(
- spacing: 8.s,
- children: <Widget>[
- CustomTextButton(
- onPressed: onCancel,
- child: Text(cancelText ?? localizations.cancelButtonLabel),
- ),
- CustomTextButton(
- onPressed: onConfirm,
- child: Text(confirmText ?? localizations.okButtonLabel),
- ),
- ],
- ),
- );
- switch (orientation) {
- case Orientation.portrait:
- return Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: <Widget>[
- header,
- Expanded(child: picker),
- actions,
- ],
- );
- case Orientation.landscape:
- return Row(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: <Widget>[
- header,
- Flexible(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: <Widget>[
- Expanded(child: picker),
- actions,
- ],
- ),
- ),
- ],
- );
- }
- }
- }
- /// Provides a pair of text fields that allow the user to enter the start and
- /// end dates that represent a range of dates.
- class _InputDateRangePicker extends StatefulWidget {
- /// Creates a row with two text fields configured to accept the start and end dates
- /// of a date range.
- _InputDateRangePicker({
- super.key,
- DateTime? initialStartDate,
- DateTime? initialEndDate,
- required DateTime firstDate,
- required DateTime lastDate,
- required this.onStartDateChanged,
- required this.onEndDateChanged,
- this.helpText,
- this.errorFormatText,
- this.errorInvalidText,
- this.errorInvalidRangeText,
- this.fieldStartHintText,
- this.fieldEndHintText,
- this.fieldStartLabelText,
- this.fieldEndLabelText,
- this.autofocus = false,
- this.autovalidate = false,
- }) : initialStartDate = initialStartDate == null
- ? null
- : DateUtils.dateOnly(initialStartDate),
- initialEndDate =
- initialEndDate == null ? null : DateUtils.dateOnly(initialEndDate),
- assert(firstDate != null),
- firstDate = DateUtils.dateOnly(firstDate),
- assert(lastDate != null),
- lastDate = DateUtils.dateOnly(lastDate),
- assert(firstDate != null),
- assert(lastDate != null),
- assert(autofocus != null),
- assert(autovalidate != null);
- /// The [DateTime] that represents the start of the initial date range selection.
- final DateTime? initialStartDate;
- /// The [DateTime] that represents the end of the initial date range selection.
- final DateTime? initialEndDate;
- /// The earliest allowable [DateTime] that the user can select.
- final DateTime firstDate;
- /// The latest allowable [DateTime] that the user can select.
- final DateTime lastDate;
- /// Called when the user changes the start date of the selected range.
- final ValueChanged<DateTime?>? onStartDateChanged;
- /// Called when the user changes the end date of the selected range.
- final ValueChanged<DateTime?>? onEndDateChanged;
- /// The text that is displayed at the top of the header.
- ///
- /// This is used to indicate to the user what they are selecting a date for.
- final String? helpText;
- /// Error text used to indicate the text in a field is not a valid date.
- final String? errorFormatText;
- /// Error text used to indicate the date in a field is not in the valid range
- /// of [firstDate] - [lastDate].
- final String? errorInvalidText;
- /// Error text used to indicate the dates given don't form a valid date
- /// range (i.e. the start date is after the end date).
- final String? errorInvalidRangeText;
- /// Hint text shown when the start date field is empty.
- final String? fieldStartHintText;
- /// Hint text shown when the end date field is empty.
- final String? fieldEndHintText;
- /// Label used for the start date field.
- final String? fieldStartLabelText;
- /// Label used for the end date field.
- final String? fieldEndLabelText;
- /// {@macro flutter.widgets.editableText.autofocus}
- final bool autofocus;
- /// If true, this the date fields will validate and update their error text
- /// immediately after every change. Otherwise, you must call
- /// [_InputDateRangePickerState.validate] to validate.
- final bool autovalidate;
- @override
- _InputDateRangePickerState createState() => _InputDateRangePickerState();
- }
- /// The current state of an [_InputDateRangePicker]. Can be used to
- /// [validate] the date field entries.
- class _InputDateRangePickerState extends State<_InputDateRangePicker> {
- late String _startInputText;
- late String _endInputText;
- DateTime? _startDate;
- DateTime? _endDate;
- late TextEditingController _startController;
- late TextEditingController _endController;
- String? _startErrorText;
- String? _endErrorText;
- bool _autoSelected = false;
- @override
- void initState() {
- super.initState();
- _startDate = widget.initialStartDate;
- _startController = TextEditingController();
- _endDate = widget.initialEndDate;
- _endController = TextEditingController();
- }
- @override
- void dispose() {
- _startController.dispose();
- _endController.dispose();
- super.dispose();
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- if (_startDate != null) {
- _startInputText = localizations.formatCompactDate(_startDate!);
- final bool selectText = widget.autofocus && !_autoSelected;
- _updateController(_startController, _startInputText, selectText);
- _autoSelected = selectText;
- }
- if (_endDate != null) {
- _endInputText = localizations.formatCompactDate(_endDate!);
- _updateController(_endController, _endInputText, false);
- }
- }
- /// Validates that the text in the start and end fields represent a valid
- /// date range.
- ///
- /// Will return true if the range is valid. If not, it will
- /// return false and display an appropriate error message under one of the
- /// text fields.
- bool validate() {
- String? startError = _validateDate(_startDate);
- final String? endError = _validateDate(_endDate);
- if (startError == null && endError == null) {
- if (_startDate!.isAfter(_endDate!)) {
- startError = widget.errorInvalidRangeText ??
- MaterialLocalizations.of(context).invalidDateRangeLabel;
- }
- }
- setState(() {
- _startErrorText = startError;
- _endErrorText = endError;
- });
- return startError == null && endError == null;
- }
- DateTime? _parseDate(String? text) {
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- return localizations.parseCompactDate(text);
- }
- String? _validateDate(DateTime? date) {
- if (date == null) {
- return widget.errorFormatText ??
- MaterialLocalizations.of(context).invalidDateFormatLabel;
- } else if (date.isBefore(widget.firstDate) ||
- date.isAfter(widget.lastDate)) {
- return widget.errorInvalidText ??
- MaterialLocalizations.of(context).dateOutOfRangeLabel;
- }
- return null;
- }
- void _updateController(
- TextEditingController controller, String text, bool selectText) {
- TextEditingValue textEditingValue = controller.value.copyWith(text: text);
- if (selectText) {
- textEditingValue = textEditingValue.copyWith(
- selection: TextSelection(
- baseOffset: 0,
- extentOffset: text.length,
- ));
- }
- controller.value = textEditingValue;
- }
- void _handleStartChanged(String text) {
- setState(() {
- _startInputText = text;
- _startDate = _parseDate(text);
- widget.onStartDateChanged?.call(_startDate);
- });
- if (widget.autovalidate) {
- validate();
- }
- }
- void _handleEndChanged(String text) {
- setState(() {
- _endInputText = text;
- _endDate = _parseDate(text);
- widget.onEndDateChanged?.call(_endDate);
- });
- if (widget.autovalidate) {
- validate();
- }
- }
- @override
- Widget build(BuildContext context) {
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final InputDecorationTheme inputTheme =
- Theme.of(context).inputDecorationTheme;
- return Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: <Widget>[
- Expanded(
- child: TextField(
- controller: _startController,
- decoration: InputDecoration(
- border: inputTheme.border ?? const UnderlineInputBorder(),
- filled: inputTheme.filled,
- hintText: widget.fieldStartHintText ?? localizations.dateHelpText,
- labelText: widget.fieldStartLabelText ??
- localizations.dateRangeStartLabel,
- errorText: _startErrorText,
- suffixIconConstraints: BoxConstraints(
- minWidth: 48.s,
- minHeight: 48.s,
- maxHeight: 48.s,
- maxWidth: 48.s),
- constraints: BoxConstraints(minHeight: 48.s, maxHeight: 48.s),
- ),
- keyboardType: TextInputType.datetime,
- onChanged: _handleStartChanged,
- autofocus: widget.autofocus,
- ),
- ),
- SizedBox(width: 8.s),
- Expanded(
- child: TextField(
- controller: _endController,
- decoration: InputDecoration(
- border: inputTheme.border ?? const UnderlineInputBorder(),
- filled: inputTheme.filled,
- hintText: widget.fieldEndHintText ?? localizations.dateHelpText,
- labelText:
- widget.fieldEndLabelText ?? localizations.dateRangeEndLabel,
- errorText: _endErrorText,
- suffixIconConstraints: BoxConstraints(
- minWidth: 48.s,
- minHeight: 48.s,
- maxHeight: 48.s,
- maxWidth: 48.s),
- constraints: BoxConstraints(minHeight: 48.s, maxHeight: 48.s),
- ),
- keyboardType: TextInputType.datetime,
- onChanged: _handleEndChanged,
- ),
- ),
- ],
- );
- }
- }
|