customdatepicker.dart 105 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065
  1. // Copyright 2014 The Flutter Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. import 'dart:math' as math;
  5. import 'package:flutter/gestures.dart' show DragStartBehavior;
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/rendering.dart';
  8. import 'package:flutter/services.dart';
  9. import 'package:flyinsonolite/controls/custom/customcalendardatepicker.dart';
  10. import 'package:flyinsonolite/controls/custom/customdialog.dart';
  11. import 'package:flyinsonolite/controls/custom/customiconbutton.dart';
  12. import 'package:flyinsonolite/controls/custom/customtextbutton.dart';
  13. import 'package:flyinsonolite/controls/text/fistext.dart';
  14. import 'package:flyinsonolite/infrastructure/scale.dart';
  15. final Size _calendarPortraitDialogSize = Size(330.s, 518.s);
  16. final Size _calendarLandscapeDialogSize = Size(496.s, 346.s);
  17. final Size _inputPortraitDialogSize = Size(330.s, 270.s);
  18. final Size _inputLandscapeDialogSize = Size(496.s, 160.s);
  19. final Size _inputRangeLandscapeDialogSize = Size(496.s, 164.s);
  20. const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200);
  21. final double _inputFormPortraitHeight = 98.s;
  22. final double _inputFormLandscapeHeight = 108.s;
  23. /// Shows a dialog containing a Material Design date picker.
  24. ///
  25. /// The returned [Future] resolves to the date selected by the user when the
  26. /// user confirms the dialog. If the user cancels the dialog, null is returned.
  27. ///
  28. /// When the date picker is first displayed, it will show the month of
  29. /// [initialDate], with [initialDate] selected.
  30. ///
  31. /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest
  32. /// allowable date. [initialDate] must either fall between these dates,
  33. /// or be equal to one of them. For each of these [DateTime] parameters, only
  34. /// their dates are considered. Their time fields are ignored. They must all
  35. /// be non-null.
  36. ///
  37. /// The [currentDate] represents the current day (i.e. today). This
  38. /// date will be highlighted in the day grid. If null, the date of
  39. /// `DateTime.now()` will be used.
  40. ///
  41. /// An optional [initialEntryMode] argument can be used to display the date
  42. /// picker in the [DatePickerEntryMode.calendar] (a calendar month grid)
  43. /// or [DatePickerEntryMode.input] (a text input field) mode.
  44. /// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
  45. ///
  46. /// An optional [selectableDayPredicate] function can be passed in to only allow
  47. /// certain days for selection. If provided, only the days that
  48. /// [selectableDayPredicate] returns true for will be selectable. For example,
  49. /// this can be used to only allow weekdays for selection. If provided, it must
  50. /// return true for [initialDate].
  51. ///
  52. /// The following optional string parameters allow you to override the default
  53. /// text used for various parts of the dialog:
  54. ///
  55. /// * [helpText], label displayed at the top of the dialog.
  56. /// * [cancelText], label on the cancel button.
  57. /// * [confirmText], label on the ok button.
  58. /// * [errorFormatText], message used when the input text isn't in a proper date format.
  59. /// * [errorInvalidText], message used when the input text isn't a selectable date.
  60. /// * [fieldHintText], text used to prompt the user when no text has been entered in the field.
  61. /// * [fieldLabelText], label for the date text input field.
  62. ///
  63. /// An optional [locale] argument can be used to set the locale for the date
  64. /// picker. It defaults to the ambient locale provided by [Localizations].
  65. ///
  66. /// An optional [textDirection] argument can be used to set the text direction
  67. /// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It
  68. /// defaults to the ambient text direction provided by [Directionality]. If both
  69. /// [locale] and [textDirection] are non-null, [textDirection] overrides the
  70. /// direction chosen for the [locale].
  71. ///
  72. /// The [context], [useRootNavigator] and [routeSettings] arguments are passed to
  73. /// [showCustomDialog], the documentation for which discusses how it is used. [context]
  74. /// and [useRootNavigator] must be non-null.
  75. ///
  76. /// The [builder] parameter can be used to wrap the dialog widget
  77. /// to add inherited widgets like [Theme].
  78. ///
  79. /// An optional [initialDatePickerMode] argument can be used to have the
  80. /// calendar date picker initially appear in the [DatePickerMode.year] or
  81. /// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day], and
  82. /// must be non-null.
  83. ///
  84. /// {@macro flutter.widgets.RawDialogRoute}
  85. ///
  86. /// ### State Restoration
  87. ///
  88. /// Using this method will not enable state restoration for the date picker.
  89. /// In order to enable state restoration for a date picker, use
  90. /// [Navigator.restorablePush] or [Navigator.restorablePushNamed] with
  91. /// [DatePickerDialog].
  92. ///
  93. /// For more information about state restoration, see [RestorationManager].
  94. ///
  95. /// {@macro flutter.widgets.RestorationManager}
  96. ///
  97. /// {@tool dartpad}
  98. /// This sample demonstrates how to create a restorable Material date picker.
  99. /// This is accomplished by enabling state restoration by specifying
  100. /// [MaterialApp.restorationScopeId] and using [Navigator.restorablePush] to
  101. /// push [DatePickerDialog] when the button is tapped.
  102. ///
  103. /// ** See code in examples/api/lib/material/date_picker/show_date_picker.0.dart **
  104. /// {@end-tool}
  105. ///
  106. /// See also:
  107. ///
  108. /// * [showDateRangePicker], which shows a Material Design date range picker
  109. /// used to select a range of dates.
  110. /// * [CustomCalendarDatePicker], which provides the calendar grid used by the date picker dialog.
  111. /// * [InputDatePickerFormField], which provides a text input field for entering dates.
  112. /// * [DisplayFeatureSubScreen], which documents the specifics of how
  113. /// [DisplayFeature]s can split the screen into sub-screens.
  114. /// * [showTimePicker], which shows a dialog that contains a Material Design time picker.
  115. ///
  116. Future<DateTime?> showCustomDatePicker({
  117. required BuildContext context,
  118. required DateTime initialDate,
  119. required DateTime firstDate,
  120. required DateTime lastDate,
  121. DateTime? currentDate,
  122. DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar,
  123. SelectableDayPredicate? selectableDayPredicate,
  124. String? helpText,
  125. String? cancelText,
  126. String? confirmText,
  127. Locale? locale,
  128. bool useRootNavigator = true,
  129. RouteSettings? routeSettings,
  130. TextDirection? textDirection,
  131. TransitionBuilder? builder,
  132. DatePickerMode initialDatePickerMode = DatePickerMode.day,
  133. String? errorFormatText,
  134. String? errorInvalidText,
  135. String? fieldHintText,
  136. String? fieldLabelText,
  137. TextInputType? keyboardType,
  138. Offset? anchorPoint,
  139. }) async {
  140. assert(context != null);
  141. assert(initialDate != null);
  142. assert(firstDate != null);
  143. assert(lastDate != null);
  144. initialDate = DateUtils.dateOnly(initialDate);
  145. firstDate = DateUtils.dateOnly(firstDate);
  146. lastDate = DateUtils.dateOnly(lastDate);
  147. assert(
  148. !lastDate.isBefore(firstDate),
  149. 'lastDate $lastDate must be on or after firstDate $firstDate.',
  150. );
  151. assert(
  152. !initialDate.isBefore(firstDate),
  153. 'initialDate $initialDate must be on or after firstDate $firstDate.',
  154. );
  155. assert(
  156. !initialDate.isAfter(lastDate),
  157. 'initialDate $initialDate must be on or before lastDate $lastDate.',
  158. );
  159. assert(
  160. selectableDayPredicate == null || selectableDayPredicate(initialDate),
  161. 'Provided initialDate $initialDate must satisfy provided selectableDayPredicate.',
  162. );
  163. assert(initialEntryMode != null);
  164. assert(useRootNavigator != null);
  165. assert(initialDatePickerMode != null);
  166. assert(debugCheckHasMaterialLocalizations(context));
  167. Widget dialog = DatePickerDialog(
  168. initialDate: initialDate,
  169. firstDate: firstDate,
  170. lastDate: lastDate,
  171. currentDate: currentDate,
  172. initialEntryMode: initialEntryMode,
  173. selectableDayPredicate: selectableDayPredicate,
  174. helpText: helpText,
  175. cancelText: cancelText,
  176. confirmText: confirmText,
  177. initialCalendarMode: initialDatePickerMode,
  178. errorFormatText: errorFormatText,
  179. errorInvalidText: errorInvalidText,
  180. fieldHintText: fieldHintText,
  181. fieldLabelText: fieldLabelText,
  182. keyboardType: keyboardType,
  183. );
  184. if (textDirection != null) {
  185. dialog = Directionality(
  186. textDirection: textDirection,
  187. child: dialog,
  188. );
  189. }
  190. if (locale != null) {
  191. dialog = Localizations.override(
  192. context: context,
  193. locale: locale,
  194. child: dialog,
  195. );
  196. }
  197. return showCustomDialog<DateTime>(
  198. context: context,
  199. useRootNavigator: useRootNavigator,
  200. routeSettings: routeSettings,
  201. builder: (BuildContext context) {
  202. return builder == null ? dialog : builder(context, dialog);
  203. },
  204. anchorPoint: anchorPoint,
  205. );
  206. }
  207. /// A Material-style date picker dialog.
  208. ///
  209. /// It is used internally by [showCustomDatePicker] or can be directly pushed
  210. /// onto the [Navigator] stack to enable state restoration. See
  211. /// [showCustomDatePicker] for a state restoration app example.
  212. ///
  213. /// See also:
  214. ///
  215. /// * [showCustomDatePicker], which is a way to display the date picker.
  216. class DatePickerDialog extends StatefulWidget {
  217. /// A Material-style date picker dialog.
  218. DatePickerDialog({
  219. super.key,
  220. required DateTime initialDate,
  221. required DateTime firstDate,
  222. required DateTime lastDate,
  223. DateTime? currentDate,
  224. this.initialEntryMode = DatePickerEntryMode.calendar,
  225. this.selectableDayPredicate,
  226. this.cancelText,
  227. this.confirmText,
  228. this.helpText,
  229. this.initialCalendarMode = DatePickerMode.day,
  230. this.errorFormatText,
  231. this.errorInvalidText,
  232. this.fieldHintText,
  233. this.fieldLabelText,
  234. this.keyboardType,
  235. this.restorationId,
  236. }) : assert(initialDate != null),
  237. assert(firstDate != null),
  238. assert(lastDate != null),
  239. initialDate = DateUtils.dateOnly(initialDate),
  240. firstDate = DateUtils.dateOnly(firstDate),
  241. lastDate = DateUtils.dateOnly(lastDate),
  242. currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
  243. assert(initialEntryMode != null),
  244. assert(initialCalendarMode != null) {
  245. assert(
  246. !this.lastDate.isBefore(this.firstDate),
  247. 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.',
  248. );
  249. assert(
  250. !this.initialDate.isBefore(this.firstDate),
  251. 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.',
  252. );
  253. assert(
  254. !this.initialDate.isAfter(this.lastDate),
  255. 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.',
  256. );
  257. assert(
  258. selectableDayPredicate == null ||
  259. selectableDayPredicate!(this.initialDate),
  260. 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate',
  261. );
  262. }
  263. /// The initially selected [DateTime] that the picker should display.
  264. final DateTime initialDate;
  265. /// The earliest allowable [DateTime] that the user can select.
  266. final DateTime firstDate;
  267. /// The latest allowable [DateTime] that the user can select.
  268. final DateTime lastDate;
  269. /// The [DateTime] representing today. It will be highlighted in the day grid.
  270. final DateTime currentDate;
  271. /// The initial mode of date entry method for the date picker dialog.
  272. ///
  273. /// See [DatePickerEntryMode] for more details on the different data entry
  274. /// modes available.
  275. final DatePickerEntryMode initialEntryMode;
  276. /// Function to provide full control over which [DateTime] can be selected.
  277. final SelectableDayPredicate? selectableDayPredicate;
  278. /// The text that is displayed on the cancel button.
  279. final String? cancelText;
  280. /// The text that is displayed on the confirm button.
  281. final String? confirmText;
  282. /// The text that is displayed at the top of the header.
  283. ///
  284. /// This is used to indicate to the user what they are selecting a date for.
  285. final String? helpText;
  286. /// The initial display of the calendar picker.
  287. final DatePickerMode initialCalendarMode;
  288. /// The error text displayed if the entered date is not in the correct format.
  289. final String? errorFormatText;
  290. /// The error text displayed if the date is not valid.
  291. ///
  292. /// A date is not valid if it is earlier than [firstDate], later than
  293. /// [lastDate], or doesn't pass the [selectableDayPredicate].
  294. final String? errorInvalidText;
  295. /// The hint text displayed in the [TextField].
  296. ///
  297. /// If this is null, it will default to the date format string. For example,
  298. /// 'mm/dd/yyyy' for en_US.
  299. final String? fieldHintText;
  300. /// The label text displayed in the [TextField].
  301. ///
  302. /// If this is null, it will default to the words representing the date format
  303. /// string. For example, 'Month, Day, Year' for en_US.
  304. final String? fieldLabelText;
  305. /// The keyboard type of the [TextField].
  306. ///
  307. /// If this is null, it will default to [TextInputType.datetime]
  308. final TextInputType? keyboardType;
  309. /// Restoration ID to save and restore the state of the [DatePickerDialog].
  310. ///
  311. /// If it is non-null, the date picker will persist and restore the
  312. /// date selected on the dialog.
  313. ///
  314. /// The state of this widget is persisted in a [RestorationBucket] claimed
  315. /// from the surrounding [RestorationScope] using the provided restoration ID.
  316. ///
  317. /// See also:
  318. ///
  319. /// * [RestorationManager], which explains how state restoration works in
  320. /// Flutter.
  321. final String? restorationId;
  322. @override
  323. State<DatePickerDialog> createState() => _DatePickerDialogState();
  324. }
  325. class _DatePickerDialogState extends State<DatePickerDialog>
  326. with RestorationMixin {
  327. late final RestorableDateTime _selectedDate =
  328. RestorableDateTime(widget.initialDate);
  329. late final _RestorableDatePickerEntryMode _entryMode =
  330. _RestorableDatePickerEntryMode(widget.initialEntryMode);
  331. final _RestorableAutovalidateMode _autovalidateMode =
  332. _RestorableAutovalidateMode(AutovalidateMode.disabled);
  333. @override
  334. String? get restorationId => widget.restorationId;
  335. @override
  336. void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
  337. registerForRestoration(_selectedDate, 'selected_date');
  338. registerForRestoration(_autovalidateMode, 'autovalidateMode');
  339. registerForRestoration(_entryMode, 'calendar_entry_mode');
  340. }
  341. final GlobalKey _calendarPickerKey = GlobalKey();
  342. final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  343. void _handleOk() {
  344. if (_entryMode.value == DatePickerEntryMode.input ||
  345. _entryMode.value == DatePickerEntryMode.inputOnly) {
  346. final FormState form = _formKey.currentState!;
  347. if (!form.validate()) {
  348. setState(() => _autovalidateMode.value = AutovalidateMode.always);
  349. return;
  350. }
  351. form.save();
  352. }
  353. Navigator.pop(context, _selectedDate.value);
  354. }
  355. void _handleCancel() {
  356. Navigator.pop(context);
  357. }
  358. void _handleEntryModeToggle() {
  359. setState(() {
  360. switch (_entryMode.value) {
  361. case DatePickerEntryMode.calendar:
  362. _autovalidateMode.value = AutovalidateMode.disabled;
  363. _entryMode.value = DatePickerEntryMode.input;
  364. break;
  365. case DatePickerEntryMode.input:
  366. _formKey.currentState!.save();
  367. _entryMode.value = DatePickerEntryMode.calendar;
  368. break;
  369. case DatePickerEntryMode.calendarOnly:
  370. case DatePickerEntryMode.inputOnly:
  371. assert(false, 'Can not change entry mode from _entryMode');
  372. break;
  373. }
  374. });
  375. }
  376. void _handleDateChanged(DateTime date) {
  377. setState(() {
  378. _selectedDate.value = date;
  379. });
  380. }
  381. Size _dialogSize(BuildContext context) {
  382. final Orientation orientation = MediaQuery.of(context).orientation;
  383. switch (_entryMode.value) {
  384. case DatePickerEntryMode.calendar:
  385. case DatePickerEntryMode.calendarOnly:
  386. switch (orientation) {
  387. case Orientation.portrait:
  388. return _calendarPortraitDialogSize;
  389. case Orientation.landscape:
  390. return _calendarLandscapeDialogSize;
  391. }
  392. case DatePickerEntryMode.input:
  393. case DatePickerEntryMode.inputOnly:
  394. switch (orientation) {
  395. case Orientation.portrait:
  396. return _inputPortraitDialogSize;
  397. case Orientation.landscape:
  398. return _inputLandscapeDialogSize;
  399. }
  400. }
  401. }
  402. static const Map<ShortcutActivator, Intent> _formShortcutMap =
  403. <ShortcutActivator, Intent>{
  404. // Pressing enter on the field will move focus to the next field or control.
  405. SingleActivator(LogicalKeyboardKey.enter): NextFocusIntent(),
  406. };
  407. @override
  408. Widget build(BuildContext context) {
  409. final ThemeData theme = Theme.of(context);
  410. final ColorScheme colorScheme = theme.colorScheme;
  411. final MaterialLocalizations localizations =
  412. MaterialLocalizations.of(context);
  413. final Orientation orientation = MediaQuery.of(context).orientation;
  414. final TextTheme textTheme = theme.textTheme;
  415. // Constrain the textScaleFactor to the largest supported value to prevent
  416. // layout issues.
  417. final double textScaleFactor =
  418. math.min(MediaQuery.of(context).textScaleFactor, 1.3);
  419. final String dateText = localizations.formatMediumDate(_selectedDate.value);
  420. final Color onPrimarySurface = colorScheme.brightness == Brightness.light
  421. ? colorScheme.onPrimary
  422. : colorScheme.onSurface;
  423. final TextStyle? dateStyle = orientation == Orientation.landscape
  424. ? textTheme.headlineSmall?.copyWith(color: onPrimarySurface)
  425. : textTheme.headlineMedium?.copyWith(color: onPrimarySurface);
  426. final Widget actions = Container(
  427. alignment: AlignmentDirectional.centerEnd,
  428. constraints: BoxConstraints(minHeight: 52.s),
  429. padding: EdgeInsets.symmetric(horizontal: 8.s),
  430. child: OverflowBar(
  431. spacing: 8.s,
  432. children: <Widget>[
  433. CustomTextButton(
  434. onPressed: _handleCancel,
  435. child:
  436. FISText(widget.cancelText ?? localizations.cancelButtonLabel),
  437. ),
  438. CustomTextButton(
  439. onPressed: _handleOk,
  440. child: FISText(widget.confirmText ?? localizations.okButtonLabel),
  441. ),
  442. ],
  443. ),
  444. );
  445. CustomCalendarDatePicker calendarDatePicker() {
  446. return CustomCalendarDatePicker(
  447. key: _calendarPickerKey,
  448. initialDate: _selectedDate.value,
  449. firstDate: widget.firstDate,
  450. lastDate: widget.lastDate,
  451. currentDate: widget.currentDate,
  452. onDateChanged: _handleDateChanged,
  453. selectableDayPredicate: widget.selectableDayPredicate,
  454. initialCalendarMode: widget.initialCalendarMode,
  455. );
  456. }
  457. Form inputDatePicker() {
  458. return Form(
  459. key: _formKey,
  460. autovalidateMode: _autovalidateMode.value,
  461. child: Container(
  462. padding: EdgeInsets.symmetric(horizontal: 24.s),
  463. height: orientation == Orientation.portrait
  464. ? _inputFormPortraitHeight
  465. : _inputFormLandscapeHeight,
  466. child: Shortcuts(
  467. shortcuts: _formShortcutMap,
  468. child: Column(
  469. children: <Widget>[
  470. const Spacer(),
  471. InputDatePickerFormField(
  472. initialDate: _selectedDate.value,
  473. firstDate: widget.firstDate,
  474. lastDate: widget.lastDate,
  475. onDateSubmitted: _handleDateChanged,
  476. onDateSaved: _handleDateChanged,
  477. selectableDayPredicate: widget.selectableDayPredicate,
  478. errorFormatText: widget.errorFormatText,
  479. errorInvalidText: widget.errorInvalidText,
  480. fieldHintText: widget.fieldHintText,
  481. fieldLabelText: widget.fieldLabelText,
  482. keyboardType: widget.keyboardType,
  483. autofocus: true,
  484. ),
  485. const Spacer(),
  486. ],
  487. ),
  488. ),
  489. ),
  490. );
  491. }
  492. final Widget picker;
  493. final Widget? entryModeButton;
  494. switch (_entryMode.value) {
  495. case DatePickerEntryMode.calendar:
  496. picker = calendarDatePicker();
  497. entryModeButton = CustomIconButton(
  498. icon: Icon(
  499. Icons.edit,
  500. size: 24.s,
  501. ),
  502. constraints: const BoxConstraints(),
  503. padding: const EdgeInsets.all(0),
  504. color: onPrimarySurface,
  505. tooltip: localizations.inputDateModeButtonLabel,
  506. onPressed: _handleEntryModeToggle,
  507. );
  508. break;
  509. case DatePickerEntryMode.calendarOnly:
  510. picker = calendarDatePicker();
  511. entryModeButton = null;
  512. break;
  513. case DatePickerEntryMode.input:
  514. picker = inputDatePicker();
  515. entryModeButton = CustomIconButton(
  516. icon: Icon(
  517. Icons.calendar_today,
  518. size: 24.s,
  519. ),
  520. constraints: const BoxConstraints(),
  521. padding: const EdgeInsets.all(0),
  522. color: onPrimarySurface,
  523. tooltip: localizations.calendarModeButtonLabel,
  524. onPressed: _handleEntryModeToggle,
  525. );
  526. break;
  527. case DatePickerEntryMode.inputOnly:
  528. picker = inputDatePicker();
  529. entryModeButton = null;
  530. break;
  531. }
  532. final Widget header = _DatePickerHeader(
  533. helpText: widget.helpText ?? localizations.datePickerHelpText,
  534. titleText: dateText,
  535. titleStyle: dateStyle,
  536. orientation: orientation,
  537. isShort: orientation == Orientation.landscape,
  538. entryModeButton: entryModeButton,
  539. );
  540. final Size dialogSize = _dialogSize(context) * textScaleFactor;
  541. return CustomDialog(
  542. insetPadding: EdgeInsets.symmetric(horizontal: 16.s, vertical: 24.s),
  543. clipBehavior: Clip.antiAlias,
  544. child: AnimatedContainer(
  545. width: dialogSize.width,
  546. height: dialogSize.height,
  547. duration: _dialogSizeAnimationDuration,
  548. curve: Curves.easeIn,
  549. child: MediaQuery(
  550. data: MediaQuery.of(context).copyWith(
  551. textScaleFactor: textScaleFactor,
  552. ),
  553. child: Builder(builder: (BuildContext context) {
  554. switch (orientation) {
  555. case Orientation.portrait:
  556. return Column(
  557. mainAxisSize: MainAxisSize.min,
  558. crossAxisAlignment: CrossAxisAlignment.stretch,
  559. children: <Widget>[
  560. header,
  561. Expanded(child: picker),
  562. actions,
  563. ],
  564. );
  565. case Orientation.landscape:
  566. return Row(
  567. mainAxisSize: MainAxisSize.min,
  568. crossAxisAlignment: CrossAxisAlignment.stretch,
  569. children: <Widget>[
  570. header,
  571. Flexible(
  572. child: Column(
  573. mainAxisSize: MainAxisSize.min,
  574. crossAxisAlignment: CrossAxisAlignment.stretch,
  575. children: <Widget>[
  576. Expanded(child: picker),
  577. actions,
  578. ],
  579. ),
  580. ),
  581. ],
  582. );
  583. }
  584. }),
  585. ),
  586. ),
  587. );
  588. }
  589. }
  590. // A restorable [DatePickerEntryMode] value.
  591. //
  592. // This serializes each entry as a unique `int` value.
  593. class _RestorableDatePickerEntryMode
  594. extends RestorableValue<DatePickerEntryMode> {
  595. _RestorableDatePickerEntryMode(
  596. DatePickerEntryMode defaultValue,
  597. ) : _defaultValue = defaultValue;
  598. final DatePickerEntryMode _defaultValue;
  599. @override
  600. DatePickerEntryMode createDefaultValue() => _defaultValue;
  601. @override
  602. void didUpdateValue(DatePickerEntryMode? oldValue) {
  603. assert(debugIsSerializableForRestoration(value.index));
  604. notifyListeners();
  605. }
  606. @override
  607. DatePickerEntryMode fromPrimitives(Object? data) =>
  608. DatePickerEntryMode.values[data! as int];
  609. @override
  610. Object? toPrimitives() => value.index;
  611. }
  612. // A restorable [AutovalidateMode] value.
  613. //
  614. // This serializes each entry as a unique `int` value.
  615. class _RestorableAutovalidateMode extends RestorableValue<AutovalidateMode> {
  616. _RestorableAutovalidateMode(
  617. AutovalidateMode defaultValue,
  618. ) : _defaultValue = defaultValue;
  619. final AutovalidateMode _defaultValue;
  620. @override
  621. AutovalidateMode createDefaultValue() => _defaultValue;
  622. @override
  623. void didUpdateValue(AutovalidateMode? oldValue) {
  624. assert(debugIsSerializableForRestoration(value.index));
  625. notifyListeners();
  626. }
  627. @override
  628. AutovalidateMode fromPrimitives(Object? data) =>
  629. AutovalidateMode.values[data! as int];
  630. @override
  631. Object? toPrimitives() => value.index;
  632. }
  633. /// Re-usable widget that displays the selected date (in large font) and the
  634. /// help text above it.
  635. ///
  636. /// These types include:
  637. ///
  638. /// * Single Date picker with calendar mode.
  639. /// * Single Date picker with text input mode.
  640. /// * Date Range picker with text input mode.
  641. ///
  642. /// [helpText], [orientation], [icon], [onIconPressed] are required and must be
  643. /// non-null.
  644. class _DatePickerHeader extends StatelessWidget {
  645. /// Creates a header for use in a date picker dialog.
  646. const _DatePickerHeader({
  647. required this.helpText,
  648. required this.titleText,
  649. this.titleSemanticsLabel,
  650. required this.titleStyle,
  651. required this.orientation,
  652. this.isShort = false,
  653. this.entryModeButton,
  654. }) : assert(helpText != null),
  655. assert(orientation != null),
  656. assert(isShort != null);
  657. double get _datePickerHeaderLandscapeWidth => 152.s;
  658. double get _datePickerHeaderPortraitHeight => 120.s;
  659. double get _headerPaddingLandscape => 16.s;
  660. /// The text that is displayed at the top of the header.
  661. ///
  662. /// This is used to indicate to the user what they are selecting a date for.
  663. final String helpText;
  664. /// The text that is displayed at the center of the header.
  665. final String titleText;
  666. /// The semantic label associated with the [titleText].
  667. final String? titleSemanticsLabel;
  668. /// The [TextStyle] that the title text is displayed with.
  669. final TextStyle? titleStyle;
  670. /// The orientation is used to decide how to layout its children.
  671. final Orientation orientation;
  672. /// Indicates the header is being displayed in a shorter/narrower context.
  673. ///
  674. /// This will be used to tighten up the space between the help text and date
  675. /// text if `true`. Additionally, it will use a smaller typography style if
  676. /// `true`.
  677. ///
  678. /// This is necessary for displaying the manual input mode in
  679. /// landscape orientation, in order to account for the keyboard height.
  680. final bool isShort;
  681. final Widget? entryModeButton;
  682. @override
  683. Widget build(BuildContext context) {
  684. final ThemeData theme = Theme.of(context);
  685. final ColorScheme colorScheme = theme.colorScheme;
  686. final TextTheme textTheme = theme.textTheme;
  687. // The header should use the primary color in light themes and surface color in dark
  688. final bool isDark = colorScheme.brightness == Brightness.dark;
  689. final Color primarySurfaceColor =
  690. isDark ? colorScheme.surface : colorScheme.primary;
  691. final Color onPrimarySurfaceColor =
  692. isDark ? colorScheme.onSurface : colorScheme.onPrimary;
  693. final TextStyle? helpStyle = textTheme.labelSmall
  694. ?.copyWith(color: onPrimarySurfaceColor, letterSpacing: 1.5.s);
  695. final Text help = Text(
  696. helpText,
  697. style: helpStyle,
  698. maxLines: 1,
  699. overflow: TextOverflow.ellipsis,
  700. );
  701. final Text title = Text(
  702. titleText,
  703. semanticsLabel: titleSemanticsLabel ?? titleText,
  704. style: titleStyle,
  705. maxLines: orientation == Orientation.portrait ? 1 : 2,
  706. overflow: TextOverflow.ellipsis,
  707. );
  708. switch (orientation) {
  709. case Orientation.portrait:
  710. return SizedBox(
  711. height: _datePickerHeaderPortraitHeight,
  712. child: Material(
  713. color: primarySurfaceColor,
  714. child: Padding(
  715. padding: EdgeInsetsDirectional.only(
  716. start: 24.s,
  717. end: 12.s,
  718. ),
  719. child: Column(
  720. crossAxisAlignment: CrossAxisAlignment.start,
  721. children: <Widget>[
  722. SizedBox(height: 16.s),
  723. help,
  724. Flexible(child: SizedBox(height: 38.s)),
  725. Row(
  726. children: <Widget>[
  727. Expanded(child: title),
  728. if (entryModeButton != null) entryModeButton!,
  729. ],
  730. ),
  731. ],
  732. ),
  733. ),
  734. ),
  735. );
  736. case Orientation.landscape:
  737. return SizedBox(
  738. width: _datePickerHeaderLandscapeWidth,
  739. child: Material(
  740. color: primarySurfaceColor,
  741. child: Column(
  742. crossAxisAlignment: CrossAxisAlignment.start,
  743. children: <Widget>[
  744. SizedBox(height: 16.s),
  745. Padding(
  746. padding: EdgeInsets.symmetric(
  747. horizontal: _headerPaddingLandscape,
  748. ),
  749. child: help,
  750. ),
  751. SizedBox(height: isShort ? 16.s : 56.s),
  752. Expanded(
  753. child: Padding(
  754. padding: EdgeInsets.symmetric(
  755. horizontal: _headerPaddingLandscape,
  756. ),
  757. child: title,
  758. ),
  759. ),
  760. if (entryModeButton != null)
  761. Padding(
  762. padding: EdgeInsets.symmetric(horizontal: 4.s),
  763. child: entryModeButton,
  764. ),
  765. ],
  766. ),
  767. ),
  768. );
  769. }
  770. }
  771. }
  772. /// Shows a full screen modal dialog containing a Material Design date range
  773. /// picker.
  774. ///
  775. /// The returned [Future] resolves to the [DateTimeRange] selected by the user
  776. /// when the user saves their selection. If the user cancels the dialog, null is
  777. /// returned.
  778. ///
  779. /// If [initialDateRange] is non-null, then it will be used as the initially
  780. /// selected date range. If it is provided, `initialDateRange.start` must be
  781. /// before or on `initialDateRange.end`.
  782. ///
  783. /// The [firstDate] is the earliest allowable date. The [lastDate] is the latest
  784. /// allowable date. Both must be non-null.
  785. ///
  786. /// If an initial date range is provided, `initialDateRange.start`
  787. /// and `initialDateRange.end` must both fall between or on [firstDate] and
  788. /// [lastDate]. For all of these [DateTime] values, only their dates are
  789. /// considered. Their time fields are ignored.
  790. ///
  791. /// The [currentDate] represents the current day (i.e. today). This
  792. /// date will be highlighted in the day grid. If null, the date of
  793. /// `DateTime.now()` will be used.
  794. ///
  795. /// An optional [initialEntryMode] argument can be used to display the date
  796. /// picker in the [DatePickerEntryMode.calendar] (a scrollable calendar month
  797. /// grid) or [DatePickerEntryMode.input] (two text input fields) mode.
  798. /// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
  799. ///
  800. /// The following optional string parameters allow you to override the default
  801. /// text used for various parts of the dialog:
  802. ///
  803. /// * [helpText], the label displayed at the top of the dialog.
  804. /// * [cancelText], the label on the cancel button for the text input mode.
  805. /// * [confirmText],the label on the ok button for the text input mode.
  806. /// * [saveText], the label on the save button for the fullscreen calendar
  807. /// mode.
  808. /// * [errorFormatText], the message used when an input text isn't in a proper
  809. /// date format.
  810. /// * [errorInvalidText], the message used when an input text isn't a
  811. /// selectable date.
  812. /// * [errorInvalidRangeText], the message used when the date range is
  813. /// invalid (e.g. start date is after end date).
  814. /// * [fieldStartHintText], the text used to prompt the user when no text has
  815. /// been entered in the start field.
  816. /// * [fieldEndHintText], the text used to prompt the user when no text has
  817. /// been entered in the end field.
  818. /// * [fieldStartLabelText], the label for the start date text input field.
  819. /// * [fieldEndLabelText], the label for the end date text input field.
  820. ///
  821. /// An optional [locale] argument can be used to set the locale for the date
  822. /// picker. It defaults to the ambient locale provided by [Localizations].
  823. ///
  824. /// An optional [textDirection] argument can be used to set the text direction
  825. /// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It
  826. /// defaults to the ambient text direction provided by [Directionality]. If both
  827. /// [locale] and [textDirection] are non-null, [textDirection] overrides the
  828. /// direction chosen for the [locale].
  829. ///
  830. /// The [context], [useRootNavigator] and [routeSettings] arguments are passed
  831. /// to [showCustomDialog], the documentation for which discusses how it is used.
  832. /// [context] and [useRootNavigator] must be non-null.
  833. ///
  834. /// The [builder] parameter can be used to wrap the dialog widget
  835. /// to add inherited widgets like [Theme].
  836. ///
  837. /// {@macro flutter.widgets.RawDialogRoute}
  838. ///
  839. /// ### State Restoration
  840. ///
  841. /// Using this method will not enable state restoration for the date range picker.
  842. /// In order to enable state restoration for a date range picker, use
  843. /// [Navigator.restorablePush] or [Navigator.restorablePushNamed] with
  844. /// [DateRangePickerDialog].
  845. ///
  846. /// For more information about state restoration, see [RestorationManager].
  847. ///
  848. /// {@macro flutter.widgets.RestorationManager}
  849. ///
  850. /// {@tool sample}
  851. /// This sample demonstrates how to create a restorable Material date range picker.
  852. /// This is accomplished by enabling state restoration by specifying
  853. /// [MaterialApp.restorationScopeId] and using [Navigator.restorablePush] to
  854. /// push [DateRangePickerDialog] when the button is tapped.
  855. ///
  856. /// ** See code in examples/api/lib/material/date_picker/show_date_range_picker.0.dart **
  857. /// {@end-tool}
  858. ///
  859. /// See also:
  860. ///
  861. /// * [showCustomDatePicker], which shows a Material Design date picker used to
  862. /// select a single date.
  863. /// * [DateTimeRange], which is used to describe a date range.
  864. /// * [DisplayFeatureSubScreen], which documents the specifics of how
  865. /// [DisplayFeature]s can split the screen into sub-screens.
  866. Future<DateTimeRange?> showDateRangePicker({
  867. required BuildContext context,
  868. DateTimeRange? initialDateRange,
  869. required DateTime firstDate,
  870. required DateTime lastDate,
  871. DateTime? currentDate,
  872. DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar,
  873. String? helpText,
  874. String? cancelText,
  875. String? confirmText,
  876. String? saveText,
  877. String? errorFormatText,
  878. String? errorInvalidText,
  879. String? errorInvalidRangeText,
  880. String? fieldStartHintText,
  881. String? fieldEndHintText,
  882. String? fieldStartLabelText,
  883. String? fieldEndLabelText,
  884. Locale? locale,
  885. bool useRootNavigator = true,
  886. RouteSettings? routeSettings,
  887. TextDirection? textDirection,
  888. TransitionBuilder? builder,
  889. Offset? anchorPoint,
  890. }) async {
  891. assert(context != null);
  892. assert(
  893. initialDateRange == null ||
  894. (initialDateRange.start != null && initialDateRange.end != null),
  895. 'initialDateRange must be null or have non-null start and end dates.',
  896. );
  897. assert(
  898. initialDateRange == null ||
  899. !initialDateRange.start.isAfter(initialDateRange.end),
  900. "initialDateRange's start date must not be after it's end date.",
  901. );
  902. initialDateRange =
  903. initialDateRange == null ? null : DateUtils.datesOnly(initialDateRange);
  904. assert(firstDate != null);
  905. firstDate = DateUtils.dateOnly(firstDate);
  906. assert(lastDate != null);
  907. lastDate = DateUtils.dateOnly(lastDate);
  908. assert(
  909. !lastDate.isBefore(firstDate),
  910. 'lastDate $lastDate must be on or after firstDate $firstDate.',
  911. );
  912. assert(
  913. initialDateRange == null || !initialDateRange.start.isBefore(firstDate),
  914. "initialDateRange's start date must be on or after firstDate $firstDate.",
  915. );
  916. assert(
  917. initialDateRange == null || !initialDateRange.end.isBefore(firstDate),
  918. "initialDateRange's end date must be on or after firstDate $firstDate.",
  919. );
  920. assert(
  921. initialDateRange == null || !initialDateRange.start.isAfter(lastDate),
  922. "initialDateRange's start date must be on or before lastDate $lastDate.",
  923. );
  924. assert(
  925. initialDateRange == null || !initialDateRange.end.isAfter(lastDate),
  926. "initialDateRange's end date must be on or before lastDate $lastDate.",
  927. );
  928. currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now());
  929. assert(initialEntryMode != null);
  930. assert(useRootNavigator != null);
  931. assert(debugCheckHasMaterialLocalizations(context));
  932. Widget dialog = DateRangePickerDialog(
  933. initialDateRange: initialDateRange,
  934. firstDate: firstDate,
  935. lastDate: lastDate,
  936. currentDate: currentDate,
  937. initialEntryMode: initialEntryMode,
  938. helpText: helpText,
  939. cancelText: cancelText,
  940. confirmText: confirmText,
  941. saveText: saveText,
  942. errorFormatText: errorFormatText,
  943. errorInvalidText: errorInvalidText,
  944. errorInvalidRangeText: errorInvalidRangeText,
  945. fieldStartHintText: fieldStartHintText,
  946. fieldEndHintText: fieldEndHintText,
  947. fieldStartLabelText: fieldStartLabelText,
  948. fieldEndLabelText: fieldEndLabelText,
  949. );
  950. if (textDirection != null) {
  951. dialog = Directionality(
  952. textDirection: textDirection,
  953. child: dialog,
  954. );
  955. }
  956. if (locale != null) {
  957. dialog = Localizations.override(
  958. context: context,
  959. locale: locale,
  960. child: dialog,
  961. );
  962. }
  963. return showCustomDialog<DateTimeRange>(
  964. context: context,
  965. useRootNavigator: useRootNavigator,
  966. routeSettings: routeSettings,
  967. useSafeArea: false,
  968. builder: (BuildContext context) {
  969. return builder == null ? dialog : builder(context, dialog);
  970. },
  971. anchorPoint: anchorPoint,
  972. );
  973. }
  974. /// Returns a locale-appropriate string to describe the start of a date range.
  975. ///
  976. /// If `startDate` is null, then it defaults to 'Start Date', otherwise if it
  977. /// is in the same year as the `endDate` then it will use the short month
  978. /// day format (i.e. 'Jan 21'). Otherwise it will return the short date format
  979. /// (i.e. 'Jan 21, 2020').
  980. String _formatRangeStartDate(MaterialLocalizations localizations,
  981. DateTime? startDate, DateTime? endDate) {
  982. return startDate == null
  983. ? localizations.dateRangeStartLabel
  984. : (endDate == null || startDate.year == endDate.year)
  985. ? localizations.formatShortMonthDay(startDate)
  986. : localizations.formatShortDate(startDate);
  987. }
  988. /// Returns an locale-appropriate string to describe the end of a date range.
  989. ///
  990. /// If `endDate` is null, then it defaults to 'End Date', otherwise if it
  991. /// is in the same year as the `startDate` and the `currentDate` then it will
  992. /// just use the short month day format (i.e. 'Jan 21'), otherwise it will
  993. /// include the year (i.e. 'Jan 21, 2020').
  994. String _formatRangeEndDate(MaterialLocalizations localizations,
  995. DateTime? startDate, DateTime? endDate, DateTime currentDate) {
  996. return endDate == null
  997. ? localizations.dateRangeEndLabel
  998. : (startDate != null &&
  999. startDate.year == endDate.year &&
  1000. startDate.year == currentDate.year)
  1001. ? localizations.formatShortMonthDay(endDate)
  1002. : localizations.formatShortDate(endDate);
  1003. }
  1004. /// A Material-style date range picker dialog.
  1005. ///
  1006. /// It is used internally by [showDateRangePicker] or can be directly pushed
  1007. /// onto the [Navigator] stack to enable state restoration. See
  1008. /// [showDateRangePicker] for a state restoration app example.
  1009. ///
  1010. /// See also:
  1011. ///
  1012. /// * [showDateRangePicker], which is a way to display the date picker.
  1013. class DateRangePickerDialog extends StatefulWidget {
  1014. /// A Material-style date range picker dialog.
  1015. const DateRangePickerDialog({
  1016. super.key,
  1017. this.initialDateRange,
  1018. required this.firstDate,
  1019. required this.lastDate,
  1020. this.currentDate,
  1021. this.initialEntryMode = DatePickerEntryMode.calendar,
  1022. this.helpText,
  1023. this.cancelText,
  1024. this.confirmText,
  1025. this.saveText,
  1026. this.errorInvalidRangeText,
  1027. this.errorFormatText,
  1028. this.errorInvalidText,
  1029. this.fieldStartHintText,
  1030. this.fieldEndHintText,
  1031. this.fieldStartLabelText,
  1032. this.fieldEndLabelText,
  1033. this.restorationId,
  1034. });
  1035. /// The date range that the date range picker starts with when it opens.
  1036. ///
  1037. /// If an initial date range is provided, `initialDateRange.start`
  1038. /// and `initialDateRange.end` must both fall between or on [firstDate] and
  1039. /// [lastDate]. For all of these [DateTime] values, only their dates are
  1040. /// considered. Their time fields are ignored.
  1041. ///
  1042. /// If [initialDateRange] is non-null, then it will be used as the initially
  1043. /// selected date range. If it is provided, `initialDateRange.start` must be
  1044. /// before or on `initialDateRange.end`.
  1045. final DateTimeRange? initialDateRange;
  1046. /// The earliest allowable date on the date range.
  1047. final DateTime firstDate;
  1048. /// The latest allowable date on the date range.
  1049. final DateTime lastDate;
  1050. /// The [currentDate] represents the current day (i.e. today).
  1051. ///
  1052. /// This date will be highlighted in the day grid.
  1053. ///
  1054. /// If `null`, the date of `DateTime.now()` will be used.
  1055. final DateTime? currentDate;
  1056. /// The initial date range picker entry mode.
  1057. ///
  1058. /// The date range has two main modes: [DatePickerEntryMode.calendar] (a
  1059. /// scrollable calendar month grid) or [DatePickerEntryMode.input] (two text
  1060. /// input fields) mode.
  1061. ///
  1062. /// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
  1063. final DatePickerEntryMode initialEntryMode;
  1064. /// The label on the cancel button for the text input mode.
  1065. ///
  1066. /// If null, the localized value of
  1067. /// [MaterialLocalizations.cancelButtonLabel] is used.
  1068. final String? cancelText;
  1069. /// The label on the "OK" button for the text input mode.
  1070. ///
  1071. /// If null, the localized value of
  1072. /// [MaterialLocalizations.okButtonLabel] is used.
  1073. final String? confirmText;
  1074. /// The label on the save button for the fullscreen calendar mode.
  1075. ///
  1076. /// If null, the localized value of
  1077. /// [MaterialLocalizations.saveButtonLabel] is used.
  1078. final String? saveText;
  1079. /// The label displayed at the top of the dialog.
  1080. ///
  1081. /// If null, the localized value of
  1082. /// [MaterialLocalizations.dateRangePickerHelpText] is used.
  1083. final String? helpText;
  1084. /// The message used when the date range is invalid (e.g. start date is after
  1085. /// end date).
  1086. ///
  1087. /// If null, the localized value of
  1088. /// [MaterialLocalizations.invalidDateRangeLabel] is used.
  1089. final String? errorInvalidRangeText;
  1090. /// The message used when an input text isn't in a proper date format.
  1091. ///
  1092. /// If null, the localized value of
  1093. /// [MaterialLocalizations.invalidDateFormatLabel] is used.
  1094. final String? errorFormatText;
  1095. /// The message used when an input text isn't a selectable date.
  1096. ///
  1097. /// If null, the localized value of
  1098. /// [MaterialLocalizations.dateOutOfRangeLabel] is used.
  1099. final String? errorInvalidText;
  1100. /// The text used to prompt the user when no text has been entered in the
  1101. /// start field.
  1102. ///
  1103. /// If null, the localized value of
  1104. /// [MaterialLocalizations.dateHelpText] is used.
  1105. final String? fieldStartHintText;
  1106. /// The text used to prompt the user when no text has been entered in the
  1107. /// end field.
  1108. ///
  1109. /// If null, the localized value of [MaterialLocalizations.dateHelpText] is
  1110. /// used.
  1111. final String? fieldEndHintText;
  1112. /// The label for the start date text input field.
  1113. ///
  1114. /// If null, the localized value of [MaterialLocalizations.dateRangeStartLabel]
  1115. /// is used.
  1116. final String? fieldStartLabelText;
  1117. /// The label for the end date text input field.
  1118. ///
  1119. /// If null, the localized value of [MaterialLocalizations.dateRangeEndLabel]
  1120. /// is used.
  1121. final String? fieldEndLabelText;
  1122. /// Restoration ID to save and restore the state of the [DateRangePickerDialog].
  1123. ///
  1124. /// If it is non-null, the date range picker will persist and restore the
  1125. /// date range selected on the dialog.
  1126. ///
  1127. /// The state of this widget is persisted in a [RestorationBucket] claimed
  1128. /// from the surrounding [RestorationScope] using the provided restoration ID.
  1129. ///
  1130. /// See also:
  1131. ///
  1132. /// * [RestorationManager], which explains how state restoration works in
  1133. /// Flutter.
  1134. final String? restorationId;
  1135. @override
  1136. State<DateRangePickerDialog> createState() => _DateRangePickerDialogState();
  1137. }
  1138. class _DateRangePickerDialogState extends State<DateRangePickerDialog>
  1139. with RestorationMixin {
  1140. late final _RestorableDatePickerEntryMode _entryMode =
  1141. _RestorableDatePickerEntryMode(widget.initialEntryMode);
  1142. late final RestorableDateTimeN _selectedStart =
  1143. RestorableDateTimeN(widget.initialDateRange?.start);
  1144. late final RestorableDateTimeN _selectedEnd =
  1145. RestorableDateTimeN(widget.initialDateRange?.end);
  1146. final RestorableBool _autoValidate = RestorableBool(false);
  1147. final GlobalKey _calendarPickerKey = GlobalKey();
  1148. final GlobalKey<_InputDateRangePickerState> _inputPickerKey =
  1149. GlobalKey<_InputDateRangePickerState>();
  1150. @override
  1151. String? get restorationId => widget.restorationId;
  1152. @override
  1153. void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
  1154. registerForRestoration(_entryMode, 'entry_mode');
  1155. registerForRestoration(_selectedStart, 'selected_start');
  1156. registerForRestoration(_selectedEnd, 'selected_end');
  1157. registerForRestoration(_autoValidate, 'autovalidate');
  1158. }
  1159. void _handleOk() {
  1160. if (_entryMode.value == DatePickerEntryMode.input ||
  1161. _entryMode.value == DatePickerEntryMode.inputOnly) {
  1162. final _InputDateRangePickerState picker = _inputPickerKey.currentState!;
  1163. if (!picker.validate()) {
  1164. setState(() {
  1165. _autoValidate.value = true;
  1166. });
  1167. return;
  1168. }
  1169. }
  1170. final DateTimeRange? selectedRange = _hasSelectedDateRange
  1171. ? DateTimeRange(start: _selectedStart.value!, end: _selectedEnd.value!)
  1172. : null;
  1173. Navigator.pop(context, selectedRange);
  1174. }
  1175. void _handleCancel() {
  1176. Navigator.pop(context);
  1177. }
  1178. void _handleEntryModeToggle() {
  1179. setState(() {
  1180. switch (_entryMode.value) {
  1181. case DatePickerEntryMode.calendar:
  1182. _autoValidate.value = false;
  1183. _entryMode.value = DatePickerEntryMode.input;
  1184. break;
  1185. case DatePickerEntryMode.input:
  1186. // Validate the range dates
  1187. if (_selectedStart.value != null &&
  1188. (_selectedStart.value!.isBefore(widget.firstDate) ||
  1189. _selectedStart.value!.isAfter(widget.lastDate))) {
  1190. _selectedStart.value = null;
  1191. // With no valid start date, having an end date makes no sense for the UI.
  1192. _selectedEnd.value = null;
  1193. }
  1194. if (_selectedEnd.value != null &&
  1195. (_selectedEnd.value!.isBefore(widget.firstDate) ||
  1196. _selectedEnd.value!.isAfter(widget.lastDate))) {
  1197. _selectedEnd.value = null;
  1198. }
  1199. // If invalid range (start after end), then just use the start date
  1200. if (_selectedStart.value != null &&
  1201. _selectedEnd.value != null &&
  1202. _selectedStart.value!.isAfter(_selectedEnd.value!)) {
  1203. _selectedEnd.value = null;
  1204. }
  1205. _entryMode.value = DatePickerEntryMode.calendar;
  1206. break;
  1207. case DatePickerEntryMode.calendarOnly:
  1208. case DatePickerEntryMode.inputOnly:
  1209. assert(false, 'Can not change entry mode from $_entryMode');
  1210. break;
  1211. }
  1212. });
  1213. }
  1214. void _handleStartDateChanged(DateTime? date) {
  1215. setState(() => _selectedStart.value = date);
  1216. }
  1217. void _handleEndDateChanged(DateTime? date) {
  1218. setState(() => _selectedEnd.value = date);
  1219. }
  1220. bool get _hasSelectedDateRange =>
  1221. _selectedStart.value != null && _selectedEnd.value != null;
  1222. @override
  1223. Widget build(BuildContext context) {
  1224. final MediaQueryData mediaQuery = MediaQuery.of(context);
  1225. final Orientation orientation = mediaQuery.orientation;
  1226. final double textScaleFactor = math.min(mediaQuery.textScaleFactor, 1.3);
  1227. final MaterialLocalizations localizations =
  1228. MaterialLocalizations.of(context);
  1229. final ColorScheme colors = Theme.of(context).colorScheme;
  1230. final Color onPrimarySurface = colors.brightness == Brightness.light
  1231. ? colors.onPrimary
  1232. : colors.onSurface;
  1233. final Widget contents;
  1234. final Size size;
  1235. ShapeBorder? shape;
  1236. final double elevation;
  1237. final EdgeInsets insetPadding;
  1238. final bool showEntryModeButton =
  1239. _entryMode.value == DatePickerEntryMode.calendar ||
  1240. _entryMode.value == DatePickerEntryMode.input;
  1241. switch (_entryMode.value) {
  1242. case DatePickerEntryMode.calendar:
  1243. case DatePickerEntryMode.calendarOnly:
  1244. contents = _CalendarRangePickerDialog(
  1245. key: _calendarPickerKey,
  1246. selectedStartDate: _selectedStart.value,
  1247. selectedEndDate: _selectedEnd.value,
  1248. firstDate: widget.firstDate,
  1249. lastDate: widget.lastDate,
  1250. currentDate: widget.currentDate,
  1251. onStartDateChanged: _handleStartDateChanged,
  1252. onEndDateChanged: _handleEndDateChanged,
  1253. onConfirm: _hasSelectedDateRange ? _handleOk : null,
  1254. onCancel: _handleCancel,
  1255. entryModeButton: showEntryModeButton
  1256. ? CustomIconButton(
  1257. icon: Icon(
  1258. Icons.edit,
  1259. size: 24.s,
  1260. ),
  1261. constraints: const BoxConstraints(),
  1262. padding: EdgeInsets.zero,
  1263. color: onPrimarySurface,
  1264. tooltip: localizations.inputDateModeButtonLabel,
  1265. onPressed: _handleEntryModeToggle,
  1266. )
  1267. : null,
  1268. confirmText: widget.saveText ?? localizations.saveButtonLabel,
  1269. helpText: widget.helpText ?? localizations.dateRangePickerHelpText,
  1270. );
  1271. size = mediaQuery.size;
  1272. insetPadding = EdgeInsets.zero;
  1273. shape = const RoundedRectangleBorder();
  1274. elevation = 0;
  1275. break;
  1276. case DatePickerEntryMode.input:
  1277. case DatePickerEntryMode.inputOnly:
  1278. contents = _InputDateRangePickerDialog(
  1279. selectedStartDate: _selectedStart.value,
  1280. selectedEndDate: _selectedEnd.value,
  1281. currentDate: widget.currentDate,
  1282. picker: Container(
  1283. padding: EdgeInsets.symmetric(horizontal: 24.s),
  1284. height: orientation == Orientation.portrait
  1285. ? _inputFormPortraitHeight
  1286. : _inputFormLandscapeHeight,
  1287. child: Column(
  1288. children: <Widget>[
  1289. const Spacer(),
  1290. _InputDateRangePicker(
  1291. key: _inputPickerKey,
  1292. initialStartDate: _selectedStart.value,
  1293. initialEndDate: _selectedEnd.value,
  1294. firstDate: widget.firstDate,
  1295. lastDate: widget.lastDate,
  1296. onStartDateChanged: _handleStartDateChanged,
  1297. onEndDateChanged: _handleEndDateChanged,
  1298. autofocus: true,
  1299. autovalidate: _autoValidate.value,
  1300. helpText: widget.helpText,
  1301. errorInvalidRangeText: widget.errorInvalidRangeText,
  1302. errorFormatText: widget.errorFormatText,
  1303. errorInvalidText: widget.errorInvalidText,
  1304. fieldStartHintText: widget.fieldStartHintText,
  1305. fieldEndHintText: widget.fieldEndHintText,
  1306. fieldStartLabelText: widget.fieldStartLabelText,
  1307. fieldEndLabelText: widget.fieldEndLabelText,
  1308. ),
  1309. const Spacer(),
  1310. ],
  1311. ),
  1312. ),
  1313. onConfirm: _handleOk,
  1314. onCancel: _handleCancel,
  1315. entryModeButton: showEntryModeButton
  1316. ? CustomIconButton(
  1317. icon: Icon(
  1318. Icons.calendar_today,
  1319. size: 24.s,
  1320. ),
  1321. padding: EdgeInsets.zero,
  1322. constraints: const BoxConstraints(),
  1323. color: onPrimarySurface,
  1324. tooltip: localizations.calendarModeButtonLabel,
  1325. onPressed: _handleEntryModeToggle,
  1326. )
  1327. : null,
  1328. confirmText: widget.confirmText ?? localizations.okButtonLabel,
  1329. cancelText: widget.cancelText ?? localizations.cancelButtonLabel,
  1330. helpText: widget.helpText ?? localizations.dateRangePickerHelpText,
  1331. );
  1332. final DialogTheme dialogTheme = Theme.of(context).dialogTheme;
  1333. size = orientation == Orientation.portrait
  1334. ? _inputPortraitDialogSize
  1335. : _inputRangeLandscapeDialogSize;
  1336. insetPadding = EdgeInsets.symmetric(horizontal: 16.s, vertical: 24.s);
  1337. shape = dialogTheme.shape;
  1338. elevation = dialogTheme.elevation ?? 24;
  1339. break;
  1340. }
  1341. return CustomDialog(
  1342. insetPadding: insetPadding,
  1343. shape: shape,
  1344. elevation: elevation,
  1345. clipBehavior: Clip.antiAlias,
  1346. child: AnimatedContainer(
  1347. width: size.width,
  1348. height: size.height,
  1349. duration: _dialogSizeAnimationDuration,
  1350. curve: Curves.easeIn,
  1351. child: MediaQuery(
  1352. data: MediaQuery.of(context).copyWith(
  1353. textScaleFactor: textScaleFactor,
  1354. ),
  1355. child: Builder(builder: (BuildContext context) {
  1356. return contents;
  1357. }),
  1358. ),
  1359. ),
  1360. );
  1361. }
  1362. }
  1363. class _CalendarRangePickerDialog extends StatelessWidget {
  1364. const _CalendarRangePickerDialog({
  1365. super.key,
  1366. required this.selectedStartDate,
  1367. required this.selectedEndDate,
  1368. required this.firstDate,
  1369. required this.lastDate,
  1370. required this.currentDate,
  1371. required this.onStartDateChanged,
  1372. required this.onEndDateChanged,
  1373. required this.onConfirm,
  1374. required this.onCancel,
  1375. required this.confirmText,
  1376. required this.helpText,
  1377. this.entryModeButton,
  1378. });
  1379. final DateTime? selectedStartDate;
  1380. final DateTime? selectedEndDate;
  1381. final DateTime firstDate;
  1382. final DateTime lastDate;
  1383. final DateTime? currentDate;
  1384. final ValueChanged<DateTime> onStartDateChanged;
  1385. final ValueChanged<DateTime?> onEndDateChanged;
  1386. final VoidCallback? onConfirm;
  1387. final VoidCallback? onCancel;
  1388. final String confirmText;
  1389. final String helpText;
  1390. final Widget? entryModeButton;
  1391. @override
  1392. Widget build(BuildContext context) {
  1393. final ThemeData theme = Theme.of(context);
  1394. final ColorScheme colorScheme = theme.colorScheme;
  1395. final MaterialLocalizations localizations =
  1396. MaterialLocalizations.of(context);
  1397. final Orientation orientation = MediaQuery.of(context).orientation;
  1398. final TextTheme textTheme = theme.textTheme;
  1399. final Color headerForeground = colorScheme.brightness == Brightness.light
  1400. ? colorScheme.onPrimary
  1401. : colorScheme.onSurface;
  1402. final Color headerDisabledForeground = headerForeground.withOpacity(0.38);
  1403. final String startDateText = _formatRangeStartDate(
  1404. localizations, selectedStartDate, selectedEndDate);
  1405. final String endDateText = _formatRangeEndDate(
  1406. localizations, selectedStartDate, selectedEndDate, DateTime.now());
  1407. final TextStyle? headlineStyle = textTheme.headlineSmall;
  1408. final TextStyle? startDateStyle = headlineStyle?.apply(
  1409. color: selectedStartDate != null
  1410. ? headerForeground
  1411. : headerDisabledForeground,
  1412. );
  1413. final TextStyle? endDateStyle = headlineStyle?.apply(
  1414. color:
  1415. selectedEndDate != null ? headerForeground : headerDisabledForeground,
  1416. );
  1417. final TextStyle saveButtonStyle = textTheme.labelLarge!.apply(
  1418. color: onConfirm != null ? headerForeground : headerDisabledForeground,
  1419. );
  1420. return SafeArea(
  1421. top: false,
  1422. left: false,
  1423. right: false,
  1424. child: Scaffold(
  1425. appBar: AppBar(
  1426. leading: CloseButton(
  1427. onPressed: onCancel,
  1428. ),
  1429. actions: <Widget>[
  1430. if (orientation == Orientation.landscape && entryModeButton != null)
  1431. entryModeButton!,
  1432. CustomTextButton(
  1433. onPressed: onConfirm,
  1434. child: Text(confirmText, style: saveButtonStyle),
  1435. ),
  1436. SizedBox(width: 8.s),
  1437. ],
  1438. bottom: PreferredSize(
  1439. preferredSize: Size(double.infinity, 64.s),
  1440. child: Row(children: <Widget>[
  1441. SizedBox(
  1442. width:
  1443. MediaQuery.of(context).size.width < 360.s ? 42.s : 72.s),
  1444. Expanded(
  1445. child: Semantics(
  1446. label: '$helpText $startDateText to $endDateText',
  1447. excludeSemantics: true,
  1448. child: Column(
  1449. crossAxisAlignment: CrossAxisAlignment.start,
  1450. children: <Widget>[
  1451. Text(
  1452. helpText,
  1453. style: textTheme.labelSmall!.apply(
  1454. color: headerForeground,
  1455. ),
  1456. ),
  1457. SizedBox(height: 8.s),
  1458. Row(
  1459. children: <Widget>[
  1460. Text(
  1461. startDateText,
  1462. style: startDateStyle,
  1463. maxLines: 1,
  1464. overflow: TextOverflow.ellipsis,
  1465. ),
  1466. Text(
  1467. ' – ',
  1468. style: startDateStyle,
  1469. ),
  1470. Flexible(
  1471. child: Text(
  1472. endDateText,
  1473. style: endDateStyle,
  1474. maxLines: 1,
  1475. overflow: TextOverflow.ellipsis,
  1476. ),
  1477. ),
  1478. ],
  1479. ),
  1480. SizedBox(height: 16.s),
  1481. ],
  1482. ),
  1483. ),
  1484. ),
  1485. if (orientation == Orientation.portrait &&
  1486. entryModeButton != null)
  1487. Padding(
  1488. padding: EdgeInsets.symmetric(horizontal: 8.s),
  1489. child: entryModeButton,
  1490. ),
  1491. ]),
  1492. ),
  1493. ),
  1494. body: _CalendarDateRangePicker(
  1495. initialStartDate: selectedStartDate,
  1496. initialEndDate: selectedEndDate,
  1497. firstDate: firstDate,
  1498. lastDate: lastDate,
  1499. currentDate: currentDate,
  1500. onStartDateChanged: onStartDateChanged,
  1501. onEndDateChanged: onEndDateChanged,
  1502. ),
  1503. ),
  1504. );
  1505. }
  1506. }
  1507. const Duration _monthScrollDuration = Duration(milliseconds: 200);
  1508. double get _monthItemHeaderHeight => 58.s;
  1509. double get _monthItemFooterHeight => 12.s;
  1510. double get _monthItemRowHeight => 42.s;
  1511. double get _monthItemSpaceBetweenRows => 8.s;
  1512. double get _horizontalPadding => 8.s;
  1513. double get _maxCalendarWidthLandscape => 384.s;
  1514. double get _maxCalendarWidthPortrait => 480.s;
  1515. /// Displays a scrollable calendar grid that allows a user to select a range
  1516. /// of dates.
  1517. class _CalendarDateRangePicker extends StatefulWidget {
  1518. /// Creates a scrollable calendar grid for picking date ranges.
  1519. _CalendarDateRangePicker({
  1520. DateTime? initialStartDate,
  1521. DateTime? initialEndDate,
  1522. required DateTime firstDate,
  1523. required DateTime lastDate,
  1524. DateTime? currentDate,
  1525. required this.onStartDateChanged,
  1526. required this.onEndDateChanged,
  1527. }) : initialStartDate = initialStartDate != null
  1528. ? DateUtils.dateOnly(initialStartDate)
  1529. : null,
  1530. initialEndDate =
  1531. initialEndDate != null ? DateUtils.dateOnly(initialEndDate) : null,
  1532. assert(firstDate != null),
  1533. assert(lastDate != null),
  1534. firstDate = DateUtils.dateOnly(firstDate),
  1535. lastDate = DateUtils.dateOnly(lastDate),
  1536. currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) {
  1537. assert(
  1538. this.initialStartDate == null ||
  1539. this.initialEndDate == null ||
  1540. !this.initialStartDate!.isAfter(initialEndDate!),
  1541. 'initialStartDate must be on or before initialEndDate.',
  1542. );
  1543. assert(
  1544. !this.lastDate.isBefore(this.firstDate),
  1545. 'firstDate must be on or before lastDate.',
  1546. );
  1547. }
  1548. /// The [DateTime] that represents the start of the initial date range selection.
  1549. final DateTime? initialStartDate;
  1550. /// The [DateTime] that represents the end of the initial date range selection.
  1551. final DateTime? initialEndDate;
  1552. /// The earliest allowable [DateTime] that the user can select.
  1553. final DateTime firstDate;
  1554. /// The latest allowable [DateTime] that the user can select.
  1555. final DateTime lastDate;
  1556. /// The [DateTime] representing today. It will be highlighted in the day grid.
  1557. final DateTime currentDate;
  1558. /// Called when the user changes the start date of the selected range.
  1559. final ValueChanged<DateTime>? onStartDateChanged;
  1560. /// Called when the user changes the end date of the selected range.
  1561. final ValueChanged<DateTime?>? onEndDateChanged;
  1562. @override
  1563. _CalendarDateRangePickerState createState() =>
  1564. _CalendarDateRangePickerState();
  1565. }
  1566. class _CalendarDateRangePickerState extends State<_CalendarDateRangePicker> {
  1567. final GlobalKey _scrollViewKey = GlobalKey();
  1568. DateTime? _startDate;
  1569. DateTime? _endDate;
  1570. int _initialMonthIndex = 0;
  1571. late ScrollController _controller;
  1572. late bool _showWeekBottomDivider;
  1573. @override
  1574. void initState() {
  1575. super.initState();
  1576. _controller = ScrollController();
  1577. _controller.addListener(_scrollListener);
  1578. _startDate = widget.initialStartDate;
  1579. _endDate = widget.initialEndDate;
  1580. // Calculate the index for the initially displayed month. This is needed to
  1581. // divide the list of months into two `SliverList`s.
  1582. final DateTime initialDate = widget.initialStartDate ?? widget.currentDate;
  1583. if (!initialDate.isBefore(widget.firstDate) &&
  1584. !initialDate.isAfter(widget.lastDate)) {
  1585. _initialMonthIndex = DateUtils.monthDelta(widget.firstDate, initialDate);
  1586. }
  1587. _showWeekBottomDivider = _initialMonthIndex != 0;
  1588. }
  1589. @override
  1590. void dispose() {
  1591. _controller.dispose();
  1592. super.dispose();
  1593. }
  1594. void _scrollListener() {
  1595. if (_controller.offset <= _controller.position.minScrollExtent) {
  1596. setState(() {
  1597. _showWeekBottomDivider = false;
  1598. });
  1599. } else if (!_showWeekBottomDivider) {
  1600. setState(() {
  1601. _showWeekBottomDivider = true;
  1602. });
  1603. }
  1604. }
  1605. int get _numberOfMonths =>
  1606. DateUtils.monthDelta(widget.firstDate, widget.lastDate) + 1;
  1607. void _vibrate() {
  1608. switch (Theme.of(context).platform) {
  1609. case TargetPlatform.android:
  1610. case TargetPlatform.fuchsia:
  1611. HapticFeedback.vibrate();
  1612. break;
  1613. case TargetPlatform.iOS:
  1614. case TargetPlatform.linux:
  1615. case TargetPlatform.macOS:
  1616. case TargetPlatform.windows:
  1617. break;
  1618. }
  1619. }
  1620. // This updates the selected date range using this logic:
  1621. //
  1622. // * From the unselected state, selecting one date creates the start date.
  1623. // * If the next selection is before the start date, reset date range and
  1624. // set the start date to that selection.
  1625. // * If the next selection is on or after the start date, set the end date
  1626. // to that selection.
  1627. // * After both start and end dates are selected, any subsequent selection
  1628. // resets the date range and sets start date to that selection.
  1629. void _updateSelection(DateTime date) {
  1630. _vibrate();
  1631. setState(() {
  1632. if (_startDate != null &&
  1633. _endDate == null &&
  1634. !date.isBefore(_startDate!)) {
  1635. _endDate = date;
  1636. widget.onEndDateChanged?.call(_endDate);
  1637. } else {
  1638. _startDate = date;
  1639. widget.onStartDateChanged?.call(_startDate!);
  1640. if (_endDate != null) {
  1641. _endDate = null;
  1642. widget.onEndDateChanged?.call(_endDate);
  1643. }
  1644. }
  1645. });
  1646. }
  1647. Widget _buildMonthItem(
  1648. BuildContext context, int index, bool beforeInitialMonth) {
  1649. final int monthIndex = beforeInitialMonth
  1650. ? _initialMonthIndex - index - 1
  1651. : _initialMonthIndex + index;
  1652. final DateTime month =
  1653. DateUtils.addMonthsToMonthDate(widget.firstDate, monthIndex);
  1654. return _MonthItem(
  1655. selectedDateStart: _startDate,
  1656. selectedDateEnd: _endDate,
  1657. currentDate: widget.currentDate,
  1658. firstDate: widget.firstDate,
  1659. lastDate: widget.lastDate,
  1660. displayedMonth: month,
  1661. onChanged: _updateSelection,
  1662. );
  1663. }
  1664. @override
  1665. Widget build(BuildContext context) {
  1666. const Key sliverAfterKey = Key('sliverAfterKey');
  1667. return Column(
  1668. children: <Widget>[
  1669. const _DayHeaders(),
  1670. if (_showWeekBottomDivider) const Divider(height: 0),
  1671. Expanded(
  1672. child: _CalendarKeyboardNavigator(
  1673. firstDate: widget.firstDate,
  1674. lastDate: widget.lastDate,
  1675. initialFocusedDay:
  1676. _startDate ?? widget.initialStartDate ?? widget.currentDate,
  1677. // In order to prevent performance issues when displaying the
  1678. // correct initial month, 2 `SliverList`s are used to split the
  1679. // months. The first item in the second SliverList is the initial
  1680. // month to be displayed.
  1681. child: CustomScrollView(
  1682. key: _scrollViewKey,
  1683. controller: _controller,
  1684. center: sliverAfterKey,
  1685. slivers: <Widget>[
  1686. SliverList(
  1687. delegate: SliverChildBuilderDelegate(
  1688. (BuildContext context, int index) =>
  1689. _buildMonthItem(context, index, true),
  1690. childCount: _initialMonthIndex,
  1691. ),
  1692. ),
  1693. SliverList(
  1694. key: sliverAfterKey,
  1695. delegate: SliverChildBuilderDelegate(
  1696. (BuildContext context, int index) =>
  1697. _buildMonthItem(context, index, false),
  1698. childCount: _numberOfMonths - _initialMonthIndex,
  1699. ),
  1700. ),
  1701. ],
  1702. ),
  1703. ),
  1704. ),
  1705. ],
  1706. );
  1707. }
  1708. }
  1709. class _CalendarKeyboardNavigator extends StatefulWidget {
  1710. const _CalendarKeyboardNavigator({
  1711. required this.child,
  1712. required this.firstDate,
  1713. required this.lastDate,
  1714. required this.initialFocusedDay,
  1715. });
  1716. final Widget child;
  1717. final DateTime firstDate;
  1718. final DateTime lastDate;
  1719. final DateTime initialFocusedDay;
  1720. @override
  1721. _CalendarKeyboardNavigatorState createState() =>
  1722. _CalendarKeyboardNavigatorState();
  1723. }
  1724. class _CalendarKeyboardNavigatorState
  1725. extends State<_CalendarKeyboardNavigator> {
  1726. final Map<ShortcutActivator, Intent> _shortcutMap =
  1727. const <ShortcutActivator, Intent>{
  1728. SingleActivator(LogicalKeyboardKey.arrowLeft):
  1729. DirectionalFocusIntent(TraversalDirection.left),
  1730. SingleActivator(LogicalKeyboardKey.arrowRight):
  1731. DirectionalFocusIntent(TraversalDirection.right),
  1732. SingleActivator(LogicalKeyboardKey.arrowDown):
  1733. DirectionalFocusIntent(TraversalDirection.down),
  1734. SingleActivator(LogicalKeyboardKey.arrowUp):
  1735. DirectionalFocusIntent(TraversalDirection.up),
  1736. };
  1737. late Map<Type, Action<Intent>> _actionMap;
  1738. late FocusNode _dayGridFocus;
  1739. TraversalDirection? _dayTraversalDirection;
  1740. DateTime? _focusedDay;
  1741. @override
  1742. void initState() {
  1743. super.initState();
  1744. _actionMap = <Type, Action<Intent>>{
  1745. NextFocusIntent:
  1746. CallbackAction<NextFocusIntent>(onInvoke: _handleGridNextFocus),
  1747. PreviousFocusIntent: CallbackAction<PreviousFocusIntent>(
  1748. onInvoke: _handleGridPreviousFocus),
  1749. DirectionalFocusIntent: CallbackAction<DirectionalFocusIntent>(
  1750. onInvoke: _handleDirectionFocus),
  1751. };
  1752. _dayGridFocus = FocusNode(debugLabel: 'Day Grid');
  1753. }
  1754. @override
  1755. void dispose() {
  1756. _dayGridFocus.dispose();
  1757. super.dispose();
  1758. }
  1759. void _handleGridFocusChange(bool focused) {
  1760. setState(() {
  1761. if (focused) {
  1762. _focusedDay ??= widget.initialFocusedDay;
  1763. }
  1764. });
  1765. }
  1766. /// Move focus to the next element after the day grid.
  1767. void _handleGridNextFocus(NextFocusIntent intent) {
  1768. _dayGridFocus.requestFocus();
  1769. _dayGridFocus.nextFocus();
  1770. }
  1771. /// Move focus to the previous element before the day grid.
  1772. void _handleGridPreviousFocus(PreviousFocusIntent intent) {
  1773. _dayGridFocus.requestFocus();
  1774. _dayGridFocus.previousFocus();
  1775. }
  1776. /// Move the internal focus date in the direction of the given intent.
  1777. ///
  1778. /// This will attempt to move the focused day to the next selectable day in
  1779. /// the given direction. If the new date is not in the current month, then
  1780. /// the page view will be scrolled to show the new date's month.
  1781. ///
  1782. /// For horizontal directions, it will move forward or backward a day (depending
  1783. /// on the current [TextDirection]). For vertical directions it will move up and
  1784. /// down a week at a time.
  1785. void _handleDirectionFocus(DirectionalFocusIntent intent) {
  1786. assert(_focusedDay != null);
  1787. setState(() {
  1788. final DateTime? nextDate =
  1789. _nextDateInDirection(_focusedDay!, intent.direction);
  1790. if (nextDate != null) {
  1791. _focusedDay = nextDate;
  1792. _dayTraversalDirection = intent.direction;
  1793. }
  1794. });
  1795. }
  1796. static const Map<TraversalDirection, int> _directionOffset =
  1797. <TraversalDirection, int>{
  1798. TraversalDirection.up: -DateTime.daysPerWeek,
  1799. TraversalDirection.right: 1,
  1800. TraversalDirection.down: DateTime.daysPerWeek,
  1801. TraversalDirection.left: -1,
  1802. };
  1803. int _dayDirectionOffset(
  1804. TraversalDirection traversalDirection, TextDirection textDirection) {
  1805. // Swap left and right if the text direction if RTL
  1806. if (textDirection == TextDirection.rtl) {
  1807. if (traversalDirection == TraversalDirection.left) {
  1808. traversalDirection = TraversalDirection.right;
  1809. } else if (traversalDirection == TraversalDirection.right) {
  1810. traversalDirection = TraversalDirection.left;
  1811. }
  1812. }
  1813. return _directionOffset[traversalDirection]!;
  1814. }
  1815. DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
  1816. final TextDirection textDirection = Directionality.of(context);
  1817. final DateTime nextDate = DateUtils.addDaysToDate(
  1818. date, _dayDirectionOffset(direction, textDirection));
  1819. if (!nextDate.isBefore(widget.firstDate) &&
  1820. !nextDate.isAfter(widget.lastDate)) {
  1821. return nextDate;
  1822. }
  1823. return null;
  1824. }
  1825. @override
  1826. Widget build(BuildContext context) {
  1827. return FocusableActionDetector(
  1828. shortcuts: _shortcutMap,
  1829. actions: _actionMap,
  1830. focusNode: _dayGridFocus,
  1831. onFocusChange: _handleGridFocusChange,
  1832. child: _FocusedDate(
  1833. date: _dayGridFocus.hasFocus ? _focusedDay : null,
  1834. scrollDirection: _dayGridFocus.hasFocus ? _dayTraversalDirection : null,
  1835. child: widget.child,
  1836. ),
  1837. );
  1838. }
  1839. }
  1840. /// InheritedWidget indicating what the current focused date is for its children.
  1841. ///
  1842. /// This is used by the [_MonthPicker] to let its children [_DayPicker]s know
  1843. /// what the currently focused date (if any) should be.
  1844. class _FocusedDate extends InheritedWidget {
  1845. const _FocusedDate({
  1846. required super.child,
  1847. this.date,
  1848. this.scrollDirection,
  1849. });
  1850. final DateTime? date;
  1851. final TraversalDirection? scrollDirection;
  1852. @override
  1853. bool updateShouldNotify(_FocusedDate oldWidget) {
  1854. return !DateUtils.isSameDay(date, oldWidget.date) ||
  1855. scrollDirection != oldWidget.scrollDirection;
  1856. }
  1857. static _FocusedDate? of(BuildContext context) {
  1858. return context.dependOnInheritedWidgetOfExactType<_FocusedDate>();
  1859. }
  1860. }
  1861. class _DayHeaders extends StatelessWidget {
  1862. const _DayHeaders();
  1863. /// Builds widgets showing abbreviated days of week. The first widget in the
  1864. /// returned list corresponds to the first day of week for the current locale.
  1865. ///
  1866. /// Examples:
  1867. ///
  1868. /// ┌ Sunday is the first day of week in the US (en_US)
  1869. /// |
  1870. /// S M T W T F S ← the returned list contains these widgets
  1871. /// _ _ _ _ _ 1 2
  1872. /// 3 4 5 6 7 8 9
  1873. ///
  1874. /// ┌ But it's Monday in the UK (en_GB)
  1875. /// |
  1876. /// M T W T F S S ← the returned list contains these widgets
  1877. /// _ _ _ _ 1 2 3
  1878. /// 4 5 6 7 8 9 10
  1879. ///
  1880. List<Widget> _getDayHeaders(
  1881. TextStyle headerStyle, MaterialLocalizations localizations) {
  1882. final List<Widget> result = <Widget>[];
  1883. for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) {
  1884. final String weekday = localizations.narrowWeekdays[i];
  1885. result.add(ExcludeSemantics(
  1886. child: Center(child: Text(weekday, style: headerStyle)),
  1887. ));
  1888. if (i == (localizations.firstDayOfWeekIndex - 1) % 7) {
  1889. break;
  1890. }
  1891. }
  1892. return result;
  1893. }
  1894. @override
  1895. Widget build(BuildContext context) {
  1896. final ThemeData themeData = Theme.of(context);
  1897. final ColorScheme colorScheme = themeData.colorScheme;
  1898. final TextStyle textStyle =
  1899. themeData.textTheme.titleSmall!.apply(color: colorScheme.onSurface);
  1900. final MaterialLocalizations localizations =
  1901. MaterialLocalizations.of(context);
  1902. final List<Widget> labels = _getDayHeaders(textStyle, localizations);
  1903. // Add leading and trailing containers for edges of the custom grid layout.
  1904. labels.insert(0, Container());
  1905. labels.add(Container());
  1906. return Container(
  1907. constraints: BoxConstraints(
  1908. maxWidth: MediaQuery.of(context).orientation == Orientation.landscape
  1909. ? _maxCalendarWidthLandscape
  1910. : _maxCalendarWidthPortrait,
  1911. maxHeight: _monthItemRowHeight,
  1912. ),
  1913. child: GridView.custom(
  1914. shrinkWrap: true,
  1915. gridDelegate: _monthItemGridDelegate,
  1916. childrenDelegate: SliverChildListDelegate(
  1917. labels,
  1918. addRepaintBoundaries: false,
  1919. ),
  1920. ),
  1921. );
  1922. }
  1923. }
  1924. class _MonthItemGridDelegate extends SliverGridDelegate {
  1925. const _MonthItemGridDelegate();
  1926. @override
  1927. SliverGridLayout getLayout(SliverConstraints constraints) {
  1928. final double tileWidth =
  1929. (constraints.crossAxisExtent - 2 * _horizontalPadding) /
  1930. DateTime.daysPerWeek;
  1931. return _MonthSliverGridLayout(
  1932. crossAxisCount: DateTime.daysPerWeek + 2,
  1933. dayChildWidth: tileWidth,
  1934. edgeChildWidth: _horizontalPadding,
  1935. reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
  1936. );
  1937. }
  1938. @override
  1939. bool shouldRelayout(_MonthItemGridDelegate oldDelegate) => false;
  1940. }
  1941. const _MonthItemGridDelegate _monthItemGridDelegate = _MonthItemGridDelegate();
  1942. class _MonthSliverGridLayout extends SliverGridLayout {
  1943. /// Creates a layout that uses equally sized and spaced tiles for each day of
  1944. /// the week and an additional edge tile for padding at the start and end of
  1945. /// each row.
  1946. ///
  1947. /// This is necessary to facilitate the painting of the range highlight
  1948. /// correctly.
  1949. const _MonthSliverGridLayout({
  1950. required this.crossAxisCount,
  1951. required this.dayChildWidth,
  1952. required this.edgeChildWidth,
  1953. required this.reverseCrossAxis,
  1954. }) : assert(crossAxisCount != null && crossAxisCount > 0),
  1955. assert(dayChildWidth != null && dayChildWidth >= 0),
  1956. assert(edgeChildWidth != null && edgeChildWidth >= 0),
  1957. assert(reverseCrossAxis != null);
  1958. /// The number of children in the cross axis.
  1959. final int crossAxisCount;
  1960. /// The width in logical pixels of the day child widgets.
  1961. final double dayChildWidth;
  1962. /// The width in logical pixels of the edge child widgets.
  1963. final double edgeChildWidth;
  1964. /// Whether the children should be placed in the opposite order of increasing
  1965. /// coordinates in the cross axis.
  1966. ///
  1967. /// For example, if the cross axis is horizontal, the children are placed from
  1968. /// left to right when [reverseCrossAxis] is false and from right to left when
  1969. /// [reverseCrossAxis] is true.
  1970. ///
  1971. /// Typically set to the return value of [axisDirectionIsReversed] applied to
  1972. /// the [SliverConstraints.crossAxisDirection].
  1973. final bool reverseCrossAxis;
  1974. /// The number of logical pixels from the leading edge of one row to the
  1975. /// leading edge of the next row.
  1976. double get _rowHeight {
  1977. return _monthItemRowHeight + _monthItemSpaceBetweenRows;
  1978. }
  1979. /// The height in logical pixels of the children widgets.
  1980. double get _childHeight {
  1981. return _monthItemRowHeight;
  1982. }
  1983. @override
  1984. int getMinChildIndexForScrollOffset(double scrollOffset) {
  1985. return crossAxisCount * (scrollOffset ~/ _rowHeight);
  1986. }
  1987. @override
  1988. int getMaxChildIndexForScrollOffset(double scrollOffset) {
  1989. final int mainAxisCount = (scrollOffset / _rowHeight).ceil();
  1990. return math.max(0, crossAxisCount * mainAxisCount - 1);
  1991. }
  1992. double _getCrossAxisOffset(double crossAxisStart, bool isPadding) {
  1993. if (reverseCrossAxis) {
  1994. return ((crossAxisCount - 2) * dayChildWidth + 2 * edgeChildWidth) -
  1995. crossAxisStart -
  1996. (isPadding ? edgeChildWidth : dayChildWidth);
  1997. }
  1998. return crossAxisStart;
  1999. }
  2000. @override
  2001. SliverGridGeometry getGeometryForChildIndex(int index) {
  2002. final int adjustedIndex = index % crossAxisCount;
  2003. final bool isEdge =
  2004. adjustedIndex == 0 || adjustedIndex == crossAxisCount - 1;
  2005. final double crossAxisStart =
  2006. math.max(0, (adjustedIndex - 1) * dayChildWidth + edgeChildWidth);
  2007. return SliverGridGeometry(
  2008. scrollOffset: (index ~/ crossAxisCount) * _rowHeight,
  2009. crossAxisOffset: _getCrossAxisOffset(crossAxisStart, isEdge),
  2010. mainAxisExtent: _childHeight,
  2011. crossAxisExtent: isEdge ? edgeChildWidth : dayChildWidth,
  2012. );
  2013. }
  2014. @override
  2015. double computeMaxScrollOffset(int childCount) {
  2016. assert(childCount >= 0);
  2017. final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1;
  2018. final double mainAxisSpacing = _rowHeight - _childHeight;
  2019. return _rowHeight * mainAxisCount - mainAxisSpacing;
  2020. }
  2021. }
  2022. /// Displays the days of a given month and allows choosing a date range.
  2023. ///
  2024. /// The days are arranged in a rectangular grid with one column for each day of
  2025. /// the week.
  2026. class _MonthItem extends StatefulWidget {
  2027. /// Creates a month item.
  2028. _MonthItem({
  2029. required this.selectedDateStart,
  2030. required this.selectedDateEnd,
  2031. required this.currentDate,
  2032. required this.onChanged,
  2033. required this.firstDate,
  2034. required this.lastDate,
  2035. required this.displayedMonth,
  2036. this.dragStartBehavior = DragStartBehavior.start,
  2037. }) : assert(firstDate != null),
  2038. assert(lastDate != null),
  2039. assert(!firstDate.isAfter(lastDate)),
  2040. assert(selectedDateStart == null ||
  2041. !selectedDateStart.isBefore(firstDate)),
  2042. assert(selectedDateEnd == null || !selectedDateEnd.isBefore(firstDate)),
  2043. assert(
  2044. selectedDateStart == null || !selectedDateStart.isAfter(lastDate)),
  2045. assert(selectedDateEnd == null || !selectedDateEnd.isAfter(lastDate)),
  2046. assert(selectedDateStart == null ||
  2047. selectedDateEnd == null ||
  2048. !selectedDateStart.isAfter(selectedDateEnd)),
  2049. assert(currentDate != null),
  2050. assert(onChanged != null),
  2051. assert(displayedMonth != null),
  2052. assert(dragStartBehavior != null);
  2053. /// The currently selected start date.
  2054. ///
  2055. /// This date is highlighted in the picker.
  2056. final DateTime? selectedDateStart;
  2057. /// The currently selected end date.
  2058. ///
  2059. /// This date is highlighted in the picker.
  2060. final DateTime? selectedDateEnd;
  2061. /// The current date at the time the picker is displayed.
  2062. final DateTime currentDate;
  2063. /// Called when the user picks a day.
  2064. final ValueChanged<DateTime> onChanged;
  2065. /// The earliest date the user is permitted to pick.
  2066. final DateTime firstDate;
  2067. /// The latest date the user is permitted to pick.
  2068. final DateTime lastDate;
  2069. /// The month whose days are displayed by this picker.
  2070. final DateTime displayedMonth;
  2071. /// Determines the way that drag start behavior is handled.
  2072. ///
  2073. /// If set to [DragStartBehavior.start], the drag gesture used to scroll a
  2074. /// date picker wheel will begin at the position where the drag gesture won
  2075. /// the arena. If set to [DragStartBehavior.down] it will begin at the position
  2076. /// where a down event is first detected.
  2077. ///
  2078. /// In general, setting this to [DragStartBehavior.start] will make drag
  2079. /// animation smoother and setting it to [DragStartBehavior.down] will make
  2080. /// drag behavior feel slightly more reactive.
  2081. ///
  2082. /// By default, the drag start behavior is [DragStartBehavior.start].
  2083. ///
  2084. /// See also:
  2085. ///
  2086. /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for
  2087. /// the different behaviors.
  2088. final DragStartBehavior dragStartBehavior;
  2089. @override
  2090. _MonthItemState createState() => _MonthItemState();
  2091. }
  2092. class _MonthItemState extends State<_MonthItem> {
  2093. /// List of [FocusNode]s, one for each day of the month.
  2094. late List<FocusNode> _dayFocusNodes;
  2095. @override
  2096. void initState() {
  2097. super.initState();
  2098. final int daysInMonth = DateUtils.getDaysInMonth(
  2099. widget.displayedMonth.year, widget.displayedMonth.month);
  2100. _dayFocusNodes = List<FocusNode>.generate(
  2101. daysInMonth,
  2102. (int index) =>
  2103. FocusNode(skipTraversal: true, debugLabel: 'Day ${index + 1}'),
  2104. );
  2105. }
  2106. @override
  2107. void didChangeDependencies() {
  2108. super.didChangeDependencies();
  2109. // Check to see if the focused date is in this month, if so focus it.
  2110. final DateTime? focusedDate = _FocusedDate.of(context)?.date;
  2111. if (focusedDate != null &&
  2112. DateUtils.isSameMonth(widget.displayedMonth, focusedDate)) {
  2113. _dayFocusNodes[focusedDate.day - 1].requestFocus();
  2114. }
  2115. }
  2116. @override
  2117. void dispose() {
  2118. for (final FocusNode node in _dayFocusNodes) {
  2119. node.dispose();
  2120. }
  2121. super.dispose();
  2122. }
  2123. Color _highlightColor(BuildContext context) {
  2124. return Theme.of(context).colorScheme.primary.withOpacity(0.12);
  2125. }
  2126. void _dayFocusChanged(bool focused) {
  2127. if (focused) {
  2128. final TraversalDirection? focusDirection =
  2129. _FocusedDate.of(context)?.scrollDirection;
  2130. if (focusDirection != null) {
  2131. ScrollPositionAlignmentPolicy policy =
  2132. ScrollPositionAlignmentPolicy.explicit;
  2133. switch (focusDirection) {
  2134. case TraversalDirection.up:
  2135. case TraversalDirection.left:
  2136. policy = ScrollPositionAlignmentPolicy.keepVisibleAtStart;
  2137. break;
  2138. case TraversalDirection.right:
  2139. case TraversalDirection.down:
  2140. policy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd;
  2141. break;
  2142. }
  2143. Scrollable.ensureVisible(
  2144. primaryFocus!.context!,
  2145. duration: _monthScrollDuration,
  2146. alignmentPolicy: policy,
  2147. );
  2148. }
  2149. }
  2150. }
  2151. Widget _buildDayItem(BuildContext context, DateTime dayToBuild,
  2152. int firstDayOffset, int daysInMonth) {
  2153. final ThemeData theme = Theme.of(context);
  2154. final ColorScheme colorScheme = theme.colorScheme;
  2155. final TextTheme textTheme = theme.textTheme;
  2156. final MaterialLocalizations localizations =
  2157. MaterialLocalizations.of(context);
  2158. final TextDirection textDirection = Directionality.of(context);
  2159. final Color highlightColor = _highlightColor(context);
  2160. final int day = dayToBuild.day;
  2161. final bool isDisabled = dayToBuild.isAfter(widget.lastDate) ||
  2162. dayToBuild.isBefore(widget.firstDate);
  2163. BoxDecoration? decoration;
  2164. TextStyle? itemStyle = textTheme.bodyMedium;
  2165. final bool isRangeSelected =
  2166. widget.selectedDateStart != null && widget.selectedDateEnd != null;
  2167. final bool isSelectedDayStart = widget.selectedDateStart != null &&
  2168. dayToBuild.isAtSameMomentAs(widget.selectedDateStart!);
  2169. final bool isSelectedDayEnd = widget.selectedDateEnd != null &&
  2170. dayToBuild.isAtSameMomentAs(widget.selectedDateEnd!);
  2171. final bool isInRange = isRangeSelected &&
  2172. dayToBuild.isAfter(widget.selectedDateStart!) &&
  2173. dayToBuild.isBefore(widget.selectedDateEnd!);
  2174. _HighlightPainter? highlightPainter;
  2175. if (isSelectedDayStart || isSelectedDayEnd) {
  2176. // The selected start and end dates gets a circle background
  2177. // highlight, and a contrasting text color.
  2178. itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.onPrimary);
  2179. decoration = BoxDecoration(
  2180. color: colorScheme.primary,
  2181. shape: BoxShape.circle,
  2182. );
  2183. if (isRangeSelected &&
  2184. widget.selectedDateStart != widget.selectedDateEnd) {
  2185. final _HighlightPainterStyle style = isSelectedDayStart
  2186. ? _HighlightPainterStyle.highlightTrailing
  2187. : _HighlightPainterStyle.highlightLeading;
  2188. highlightPainter = _HighlightPainter(
  2189. color: highlightColor,
  2190. style: style,
  2191. textDirection: textDirection,
  2192. );
  2193. }
  2194. } else if (isInRange) {
  2195. // The days within the range get a light background highlight.
  2196. highlightPainter = _HighlightPainter(
  2197. color: highlightColor,
  2198. style: _HighlightPainterStyle.highlightAll,
  2199. textDirection: textDirection,
  2200. );
  2201. } else if (isDisabled) {
  2202. itemStyle = textTheme.bodyMedium
  2203. ?.apply(color: colorScheme.onSurface.withOpacity(0.38));
  2204. } else if (DateUtils.isSameDay(widget.currentDate, dayToBuild)) {
  2205. // The current day gets a different text color and a circle stroke
  2206. // border.
  2207. itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.primary);
  2208. decoration = BoxDecoration(
  2209. border: Border.all(color: colorScheme.primary, width: 1.s),
  2210. shape: BoxShape.circle,
  2211. );
  2212. }
  2213. // We want the day of month to be spoken first irrespective of the
  2214. // locale-specific preferences or TextDirection. This is because
  2215. // an accessibility user is more likely to be interested in the
  2216. // day of month before the rest of the date, as they are looking
  2217. // for the day of month. To do that we prepend day of month to the
  2218. // formatted full date.
  2219. String semanticLabel =
  2220. '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}';
  2221. if (isSelectedDayStart) {
  2222. semanticLabel =
  2223. localizations.dateRangeStartDateSemanticLabel(semanticLabel);
  2224. } else if (isSelectedDayEnd) {
  2225. semanticLabel =
  2226. localizations.dateRangeEndDateSemanticLabel(semanticLabel);
  2227. }
  2228. Widget dayWidget = Container(
  2229. decoration: decoration,
  2230. child: Center(
  2231. child: Semantics(
  2232. label: semanticLabel,
  2233. selected: isSelectedDayStart || isSelectedDayEnd,
  2234. child: ExcludeSemantics(
  2235. child: Text(localizations.formatDecimal(day), style: itemStyle),
  2236. ),
  2237. ),
  2238. ),
  2239. );
  2240. if (highlightPainter != null) {
  2241. dayWidget = CustomPaint(
  2242. painter: highlightPainter,
  2243. child: dayWidget,
  2244. );
  2245. }
  2246. if (!isDisabled) {
  2247. dayWidget = InkResponse(
  2248. focusNode: _dayFocusNodes[day - 1],
  2249. onTap: () => widget.onChanged(dayToBuild),
  2250. radius: _monthItemRowHeight / 2 + 4.s,
  2251. splashColor: colorScheme.primary.withOpacity(0.38),
  2252. onFocusChange: _dayFocusChanged,
  2253. child: dayWidget,
  2254. );
  2255. }
  2256. return dayWidget;
  2257. }
  2258. Widget _buildEdgeContainer(BuildContext context, bool isHighlighted) {
  2259. return Container(color: isHighlighted ? _highlightColor(context) : null);
  2260. }
  2261. @override
  2262. Widget build(BuildContext context) {
  2263. final ThemeData themeData = Theme.of(context);
  2264. final TextTheme textTheme = themeData.textTheme;
  2265. final MaterialLocalizations localizations =
  2266. MaterialLocalizations.of(context);
  2267. final int year = widget.displayedMonth.year;
  2268. final int month = widget.displayedMonth.month;
  2269. final int daysInMonth = DateUtils.getDaysInMonth(year, month);
  2270. final int dayOffset = DateUtils.firstDayOffset(year, month, localizations);
  2271. final int weeks = ((daysInMonth + dayOffset) / DateTime.daysPerWeek).ceil();
  2272. final double gridHeight =
  2273. weeks * _monthItemRowHeight + (weeks - 1) * _monthItemSpaceBetweenRows;
  2274. final List<Widget> dayItems = <Widget>[];
  2275. for (int i = 0; true; i += 1) {
  2276. // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
  2277. // a leap year.
  2278. final int day = i - dayOffset + 1;
  2279. if (day > daysInMonth) {
  2280. break;
  2281. }
  2282. if (day < 1) {
  2283. dayItems.add(Container());
  2284. } else {
  2285. final DateTime dayToBuild = DateTime(year, month, day);
  2286. final Widget dayItem = _buildDayItem(
  2287. context,
  2288. dayToBuild,
  2289. dayOffset,
  2290. daysInMonth,
  2291. );
  2292. dayItems.add(dayItem);
  2293. }
  2294. }
  2295. // Add the leading/trailing edge containers to each week in order to
  2296. // correctly extend the range highlight.
  2297. final List<Widget> paddedDayItems = <Widget>[];
  2298. for (int i = 0; i < weeks; i++) {
  2299. final int start = i * DateTime.daysPerWeek;
  2300. final int end = math.min(
  2301. start + DateTime.daysPerWeek,
  2302. dayItems.length,
  2303. );
  2304. final List<Widget> weekList = dayItems.sublist(start, end);
  2305. final DateTime dateAfterLeadingPadding =
  2306. DateTime(year, month, start - dayOffset + 1);
  2307. // Only color the edge container if it is after the start date and
  2308. // on/before the end date.
  2309. final bool isLeadingInRange = !(dayOffset > 0 && i == 0) &&
  2310. widget.selectedDateStart != null &&
  2311. widget.selectedDateEnd != null &&
  2312. dateAfterLeadingPadding.isAfter(widget.selectedDateStart!) &&
  2313. !dateAfterLeadingPadding.isAfter(widget.selectedDateEnd!);
  2314. weekList.insert(0, _buildEdgeContainer(context, isLeadingInRange));
  2315. // Only add a trailing edge container if it is for a full week and not a
  2316. // partial week.
  2317. if (end < dayItems.length ||
  2318. (end == dayItems.length &&
  2319. dayItems.length % DateTime.daysPerWeek == 0)) {
  2320. final DateTime dateBeforeTrailingPadding =
  2321. DateTime(year, month, end - dayOffset);
  2322. // Only color the edge container if it is on/after the start date and
  2323. // before the end date.
  2324. final bool isTrailingInRange = widget.selectedDateStart != null &&
  2325. widget.selectedDateEnd != null &&
  2326. !dateBeforeTrailingPadding.isBefore(widget.selectedDateStart!) &&
  2327. dateBeforeTrailingPadding.isBefore(widget.selectedDateEnd!);
  2328. weekList.add(_buildEdgeContainer(context, isTrailingInRange));
  2329. }
  2330. paddedDayItems.addAll(weekList);
  2331. }
  2332. final double maxWidth =
  2333. MediaQuery.of(context).orientation == Orientation.landscape
  2334. ? _maxCalendarWidthLandscape
  2335. : _maxCalendarWidthPortrait;
  2336. return Column(
  2337. children: <Widget>[
  2338. Container(
  2339. constraints: BoxConstraints(maxWidth: maxWidth),
  2340. height: _monthItemHeaderHeight,
  2341. padding: EdgeInsets.symmetric(horizontal: 16.s),
  2342. alignment: AlignmentDirectional.centerStart,
  2343. child: ExcludeSemantics(
  2344. child: Text(
  2345. localizations.formatMonthYear(widget.displayedMonth),
  2346. style: textTheme.bodyMedium!
  2347. .apply(color: themeData.colorScheme.onSurface),
  2348. ),
  2349. ),
  2350. ),
  2351. Container(
  2352. constraints: BoxConstraints(
  2353. maxWidth: maxWidth,
  2354. maxHeight: gridHeight,
  2355. ),
  2356. child: GridView.custom(
  2357. physics: const NeverScrollableScrollPhysics(),
  2358. gridDelegate: _monthItemGridDelegate,
  2359. childrenDelegate: SliverChildListDelegate(
  2360. paddedDayItems,
  2361. addRepaintBoundaries: false,
  2362. ),
  2363. ),
  2364. ),
  2365. SizedBox(height: _monthItemFooterHeight),
  2366. ],
  2367. );
  2368. }
  2369. }
  2370. /// Determines which style to use to paint the highlight.
  2371. enum _HighlightPainterStyle {
  2372. /// Paints nothing.
  2373. none,
  2374. /// Paints a rectangle that occupies the leading half of the space.
  2375. highlightLeading,
  2376. /// Paints a rectangle that occupies the trailing half of the space.
  2377. highlightTrailing,
  2378. /// Paints a rectangle that occupies all available space.
  2379. highlightAll,
  2380. }
  2381. /// This custom painter will add a background highlight to its child.
  2382. ///
  2383. /// This highlight will be drawn depending on the [style], [color], and
  2384. /// [textDirection] supplied. It will either paint a rectangle on the
  2385. /// left/right, a full rectangle, or nothing at all. This logic is determined by
  2386. /// a combination of the [style] and [textDirection].
  2387. class _HighlightPainter extends CustomPainter {
  2388. _HighlightPainter({
  2389. required this.color,
  2390. this.style = _HighlightPainterStyle.none,
  2391. this.textDirection,
  2392. });
  2393. final Color color;
  2394. final _HighlightPainterStyle style;
  2395. final TextDirection? textDirection;
  2396. @override
  2397. void paint(Canvas canvas, Size size) {
  2398. if (style == _HighlightPainterStyle.none) {
  2399. return;
  2400. }
  2401. final Paint paint = Paint()
  2402. ..color = color
  2403. ..style = PaintingStyle.fill;
  2404. final Rect rectLeft = Rect.fromLTWH(0, 0, size.width / 2, size.height);
  2405. final Rect rectRight =
  2406. Rect.fromLTWH(size.width / 2, 0, size.width / 2, size.height);
  2407. switch (style) {
  2408. case _HighlightPainterStyle.highlightTrailing:
  2409. canvas.drawRect(
  2410. textDirection == TextDirection.ltr ? rectRight : rectLeft,
  2411. paint,
  2412. );
  2413. break;
  2414. case _HighlightPainterStyle.highlightLeading:
  2415. canvas.drawRect(
  2416. textDirection == TextDirection.ltr ? rectLeft : rectRight,
  2417. paint,
  2418. );
  2419. break;
  2420. case _HighlightPainterStyle.highlightAll:
  2421. canvas.drawRect(
  2422. Rect.fromLTWH(0, 0, size.width, size.height),
  2423. paint,
  2424. );
  2425. break;
  2426. case _HighlightPainterStyle.none:
  2427. break;
  2428. }
  2429. }
  2430. @override
  2431. bool shouldRepaint(CustomPainter oldDelegate) => false;
  2432. }
  2433. class _InputDateRangePickerDialog extends StatelessWidget {
  2434. const _InputDateRangePickerDialog({
  2435. required this.selectedStartDate,
  2436. required this.selectedEndDate,
  2437. required this.currentDate,
  2438. required this.picker,
  2439. required this.onConfirm,
  2440. required this.onCancel,
  2441. required this.confirmText,
  2442. required this.cancelText,
  2443. required this.helpText,
  2444. required this.entryModeButton,
  2445. });
  2446. final DateTime? selectedStartDate;
  2447. final DateTime? selectedEndDate;
  2448. final DateTime? currentDate;
  2449. final Widget picker;
  2450. final VoidCallback onConfirm;
  2451. final VoidCallback onCancel;
  2452. final String? confirmText;
  2453. final String? cancelText;
  2454. final String? helpText;
  2455. final Widget? entryModeButton;
  2456. String _formatDateRange(
  2457. BuildContext context, DateTime? start, DateTime? end, DateTime now) {
  2458. final MaterialLocalizations localizations =
  2459. MaterialLocalizations.of(context);
  2460. final String startText = _formatRangeStartDate(localizations, start, end);
  2461. final String endText = _formatRangeEndDate(localizations, start, end, now);
  2462. if (start == null || end == null) {
  2463. return localizations.unspecifiedDateRange;
  2464. }
  2465. if (Directionality.of(context) == TextDirection.ltr) {
  2466. return '$startText – $endText';
  2467. } else {
  2468. return '$endText – $startText';
  2469. }
  2470. }
  2471. @override
  2472. Widget build(BuildContext context) {
  2473. final ThemeData theme = Theme.of(context);
  2474. final ColorScheme colorScheme = theme.colorScheme;
  2475. final MaterialLocalizations localizations =
  2476. MaterialLocalizations.of(context);
  2477. final Orientation orientation = MediaQuery.of(context).orientation;
  2478. final TextTheme textTheme = theme.textTheme;
  2479. final Color onPrimarySurfaceColor =
  2480. colorScheme.brightness == Brightness.light
  2481. ? colorScheme.onPrimary
  2482. : colorScheme.onSurface;
  2483. final TextStyle? dateStyle = orientation == Orientation.landscape
  2484. ? textTheme.headlineSmall?.apply(color: onPrimarySurfaceColor)
  2485. : textTheme.headlineMedium?.apply(color: onPrimarySurfaceColor);
  2486. final String dateText = _formatDateRange(
  2487. context, selectedStartDate, selectedEndDate, currentDate!);
  2488. final String semanticDateText = selectedStartDate != null &&
  2489. selectedEndDate != null
  2490. ? '${localizations.formatMediumDate(selectedStartDate!)} – ${localizations.formatMediumDate(selectedEndDate!)}'
  2491. : '';
  2492. final Widget header = _DatePickerHeader(
  2493. helpText: helpText ?? localizations.dateRangePickerHelpText,
  2494. titleText: dateText,
  2495. titleSemanticsLabel: semanticDateText,
  2496. titleStyle: dateStyle,
  2497. orientation: orientation,
  2498. isShort: orientation == Orientation.landscape,
  2499. entryModeButton: entryModeButton,
  2500. );
  2501. final Widget actions = Container(
  2502. alignment: AlignmentDirectional.centerEnd,
  2503. constraints: BoxConstraints(minHeight: 52.s),
  2504. padding: EdgeInsets.symmetric(horizontal: 8.s),
  2505. child: OverflowBar(
  2506. spacing: 8.s,
  2507. children: <Widget>[
  2508. CustomTextButton(
  2509. onPressed: onCancel,
  2510. child: Text(cancelText ?? localizations.cancelButtonLabel),
  2511. ),
  2512. CustomTextButton(
  2513. onPressed: onConfirm,
  2514. child: Text(confirmText ?? localizations.okButtonLabel),
  2515. ),
  2516. ],
  2517. ),
  2518. );
  2519. switch (orientation) {
  2520. case Orientation.portrait:
  2521. return Column(
  2522. mainAxisSize: MainAxisSize.min,
  2523. crossAxisAlignment: CrossAxisAlignment.stretch,
  2524. children: <Widget>[
  2525. header,
  2526. Expanded(child: picker),
  2527. actions,
  2528. ],
  2529. );
  2530. case Orientation.landscape:
  2531. return Row(
  2532. mainAxisSize: MainAxisSize.min,
  2533. crossAxisAlignment: CrossAxisAlignment.stretch,
  2534. children: <Widget>[
  2535. header,
  2536. Flexible(
  2537. child: Column(
  2538. mainAxisSize: MainAxisSize.min,
  2539. crossAxisAlignment: CrossAxisAlignment.stretch,
  2540. children: <Widget>[
  2541. Expanded(child: picker),
  2542. actions,
  2543. ],
  2544. ),
  2545. ),
  2546. ],
  2547. );
  2548. }
  2549. }
  2550. }
  2551. /// Provides a pair of text fields that allow the user to enter the start and
  2552. /// end dates that represent a range of dates.
  2553. class _InputDateRangePicker extends StatefulWidget {
  2554. /// Creates a row with two text fields configured to accept the start and end dates
  2555. /// of a date range.
  2556. _InputDateRangePicker({
  2557. super.key,
  2558. DateTime? initialStartDate,
  2559. DateTime? initialEndDate,
  2560. required DateTime firstDate,
  2561. required DateTime lastDate,
  2562. required this.onStartDateChanged,
  2563. required this.onEndDateChanged,
  2564. this.helpText,
  2565. this.errorFormatText,
  2566. this.errorInvalidText,
  2567. this.errorInvalidRangeText,
  2568. this.fieldStartHintText,
  2569. this.fieldEndHintText,
  2570. this.fieldStartLabelText,
  2571. this.fieldEndLabelText,
  2572. this.autofocus = false,
  2573. this.autovalidate = false,
  2574. }) : initialStartDate = initialStartDate == null
  2575. ? null
  2576. : DateUtils.dateOnly(initialStartDate),
  2577. initialEndDate =
  2578. initialEndDate == null ? null : DateUtils.dateOnly(initialEndDate),
  2579. assert(firstDate != null),
  2580. firstDate = DateUtils.dateOnly(firstDate),
  2581. assert(lastDate != null),
  2582. lastDate = DateUtils.dateOnly(lastDate),
  2583. assert(firstDate != null),
  2584. assert(lastDate != null),
  2585. assert(autofocus != null),
  2586. assert(autovalidate != null);
  2587. /// The [DateTime] that represents the start of the initial date range selection.
  2588. final DateTime? initialStartDate;
  2589. /// The [DateTime] that represents the end of the initial date range selection.
  2590. final DateTime? initialEndDate;
  2591. /// The earliest allowable [DateTime] that the user can select.
  2592. final DateTime firstDate;
  2593. /// The latest allowable [DateTime] that the user can select.
  2594. final DateTime lastDate;
  2595. /// Called when the user changes the start date of the selected range.
  2596. final ValueChanged<DateTime?>? onStartDateChanged;
  2597. /// Called when the user changes the end date of the selected range.
  2598. final ValueChanged<DateTime?>? onEndDateChanged;
  2599. /// The text that is displayed at the top of the header.
  2600. ///
  2601. /// This is used to indicate to the user what they are selecting a date for.
  2602. final String? helpText;
  2603. /// Error text used to indicate the text in a field is not a valid date.
  2604. final String? errorFormatText;
  2605. /// Error text used to indicate the date in a field is not in the valid range
  2606. /// of [firstDate] - [lastDate].
  2607. final String? errorInvalidText;
  2608. /// Error text used to indicate the dates given don't form a valid date
  2609. /// range (i.e. the start date is after the end date).
  2610. final String? errorInvalidRangeText;
  2611. /// Hint text shown when the start date field is empty.
  2612. final String? fieldStartHintText;
  2613. /// Hint text shown when the end date field is empty.
  2614. final String? fieldEndHintText;
  2615. /// Label used for the start date field.
  2616. final String? fieldStartLabelText;
  2617. /// Label used for the end date field.
  2618. final String? fieldEndLabelText;
  2619. /// {@macro flutter.widgets.editableText.autofocus}
  2620. final bool autofocus;
  2621. /// If true, this the date fields will validate and update their error text
  2622. /// immediately after every change. Otherwise, you must call
  2623. /// [_InputDateRangePickerState.validate] to validate.
  2624. final bool autovalidate;
  2625. @override
  2626. _InputDateRangePickerState createState() => _InputDateRangePickerState();
  2627. }
  2628. /// The current state of an [_InputDateRangePicker]. Can be used to
  2629. /// [validate] the date field entries.
  2630. class _InputDateRangePickerState extends State<_InputDateRangePicker> {
  2631. late String _startInputText;
  2632. late String _endInputText;
  2633. DateTime? _startDate;
  2634. DateTime? _endDate;
  2635. late TextEditingController _startController;
  2636. late TextEditingController _endController;
  2637. String? _startErrorText;
  2638. String? _endErrorText;
  2639. bool _autoSelected = false;
  2640. @override
  2641. void initState() {
  2642. super.initState();
  2643. _startDate = widget.initialStartDate;
  2644. _startController = TextEditingController();
  2645. _endDate = widget.initialEndDate;
  2646. _endController = TextEditingController();
  2647. }
  2648. @override
  2649. void dispose() {
  2650. _startController.dispose();
  2651. _endController.dispose();
  2652. super.dispose();
  2653. }
  2654. @override
  2655. void didChangeDependencies() {
  2656. super.didChangeDependencies();
  2657. final MaterialLocalizations localizations =
  2658. MaterialLocalizations.of(context);
  2659. if (_startDate != null) {
  2660. _startInputText = localizations.formatCompactDate(_startDate!);
  2661. final bool selectText = widget.autofocus && !_autoSelected;
  2662. _updateController(_startController, _startInputText, selectText);
  2663. _autoSelected = selectText;
  2664. }
  2665. if (_endDate != null) {
  2666. _endInputText = localizations.formatCompactDate(_endDate!);
  2667. _updateController(_endController, _endInputText, false);
  2668. }
  2669. }
  2670. /// Validates that the text in the start and end fields represent a valid
  2671. /// date range.
  2672. ///
  2673. /// Will return true if the range is valid. If not, it will
  2674. /// return false and display an appropriate error message under one of the
  2675. /// text fields.
  2676. bool validate() {
  2677. String? startError = _validateDate(_startDate);
  2678. final String? endError = _validateDate(_endDate);
  2679. if (startError == null && endError == null) {
  2680. if (_startDate!.isAfter(_endDate!)) {
  2681. startError = widget.errorInvalidRangeText ??
  2682. MaterialLocalizations.of(context).invalidDateRangeLabel;
  2683. }
  2684. }
  2685. setState(() {
  2686. _startErrorText = startError;
  2687. _endErrorText = endError;
  2688. });
  2689. return startError == null && endError == null;
  2690. }
  2691. DateTime? _parseDate(String? text) {
  2692. final MaterialLocalizations localizations =
  2693. MaterialLocalizations.of(context);
  2694. return localizations.parseCompactDate(text);
  2695. }
  2696. String? _validateDate(DateTime? date) {
  2697. if (date == null) {
  2698. return widget.errorFormatText ??
  2699. MaterialLocalizations.of(context).invalidDateFormatLabel;
  2700. } else if (date.isBefore(widget.firstDate) ||
  2701. date.isAfter(widget.lastDate)) {
  2702. return widget.errorInvalidText ??
  2703. MaterialLocalizations.of(context).dateOutOfRangeLabel;
  2704. }
  2705. return null;
  2706. }
  2707. void _updateController(
  2708. TextEditingController controller, String text, bool selectText) {
  2709. TextEditingValue textEditingValue = controller.value.copyWith(text: text);
  2710. if (selectText) {
  2711. textEditingValue = textEditingValue.copyWith(
  2712. selection: TextSelection(
  2713. baseOffset: 0,
  2714. extentOffset: text.length,
  2715. ));
  2716. }
  2717. controller.value = textEditingValue;
  2718. }
  2719. void _handleStartChanged(String text) {
  2720. setState(() {
  2721. _startInputText = text;
  2722. _startDate = _parseDate(text);
  2723. widget.onStartDateChanged?.call(_startDate);
  2724. });
  2725. if (widget.autovalidate) {
  2726. validate();
  2727. }
  2728. }
  2729. void _handleEndChanged(String text) {
  2730. setState(() {
  2731. _endInputText = text;
  2732. _endDate = _parseDate(text);
  2733. widget.onEndDateChanged?.call(_endDate);
  2734. });
  2735. if (widget.autovalidate) {
  2736. validate();
  2737. }
  2738. }
  2739. @override
  2740. Widget build(BuildContext context) {
  2741. final MaterialLocalizations localizations =
  2742. MaterialLocalizations.of(context);
  2743. final InputDecorationTheme inputTheme =
  2744. Theme.of(context).inputDecorationTheme;
  2745. return Row(
  2746. crossAxisAlignment: CrossAxisAlignment.start,
  2747. children: <Widget>[
  2748. Expanded(
  2749. child: TextField(
  2750. controller: _startController,
  2751. decoration: InputDecoration(
  2752. border: inputTheme.border ?? const UnderlineInputBorder(),
  2753. filled: inputTheme.filled,
  2754. hintText: widget.fieldStartHintText ?? localizations.dateHelpText,
  2755. labelText: widget.fieldStartLabelText ??
  2756. localizations.dateRangeStartLabel,
  2757. errorText: _startErrorText,
  2758. suffixIconConstraints: BoxConstraints(
  2759. minWidth: 48.s,
  2760. minHeight: 48.s,
  2761. maxHeight: 48.s,
  2762. maxWidth: 48.s),
  2763. constraints: BoxConstraints(minHeight: 48.s, maxHeight: 48.s),
  2764. ),
  2765. keyboardType: TextInputType.datetime,
  2766. onChanged: _handleStartChanged,
  2767. autofocus: widget.autofocus,
  2768. ),
  2769. ),
  2770. SizedBox(width: 8.s),
  2771. Expanded(
  2772. child: TextField(
  2773. controller: _endController,
  2774. decoration: InputDecoration(
  2775. border: inputTheme.border ?? const UnderlineInputBorder(),
  2776. filled: inputTheme.filled,
  2777. hintText: widget.fieldEndHintText ?? localizations.dateHelpText,
  2778. labelText:
  2779. widget.fieldEndLabelText ?? localizations.dateRangeEndLabel,
  2780. errorText: _endErrorText,
  2781. suffixIconConstraints: BoxConstraints(
  2782. minWidth: 48.s,
  2783. minHeight: 48.s,
  2784. maxHeight: 48.s,
  2785. maxWidth: 48.s),
  2786. constraints: BoxConstraints(minHeight: 48.s, maxHeight: 48.s),
  2787. ),
  2788. keyboardType: TextInputType.datetime,
  2789. onChanged: _handleEndChanged,
  2790. ),
  2791. ),
  2792. ],
  2793. );
  2794. }
  2795. }