custom_date_picker.dart 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  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. // ignore_for_file: implementation_imports
  5. import 'dart:math' as math;
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/scheduler.dart';
  8. import 'package:flutter/src/cupertino/picker.dart';
  9. // Values derived from https://developer.apple.com/design/resources/ and on iOS
  10. // simulators with "Debug View Hierarchy".
  11. const double _kItemExtent = 56.0;
  12. // From the picker's intrinsic content size constraint.
  13. const double _kPickerWidth = 380.0;
  14. const bool _kUseMagnifier = true;
  15. // const double _kMagnification = 2.35 / 2.1;
  16. const double _kMagnification = 1.06;
  17. const double _kDatePickerPadSize = 12.0;
  18. // The density of a date picker is different from a generic picker.
  19. // Eyeballed from iOS.
  20. const double _kSqueeze = 1.25;
  21. const TextStyle _kDefaultPickerTextStyle = TextStyle(
  22. letterSpacing: -0.83,
  23. );
  24. TextStyle _themeTextStyle(BuildContext context, {bool isValid = true}) {
  25. return isValid
  26. ? const TextStyle(color: Colors.black, fontSize: 24)
  27. : TextStyle(color: Colors.grey[700], fontSize: 22);
  28. }
  29. void _animateColumnControllerToItem(
  30. FixedExtentScrollController controller, int targetItem) {
  31. controller.animateToItem(
  32. targetItem,
  33. curve: Curves.easeInOut,
  34. duration: const Duration(milliseconds: 200),
  35. );
  36. }
  37. const Widget _startSelectionOverlay = CupertinoPickerDefaultSelectionOverlay(
  38. capEndEdge: false,
  39. background: Colors.transparent,
  40. );
  41. const Widget _centerSelectionOverlay = CupertinoPickerDefaultSelectionOverlay(
  42. capStartEdge: false,
  43. capEndEdge: false,
  44. background: Colors.transparent,
  45. );
  46. const Widget _endSelectionOverlay = CupertinoPickerDefaultSelectionOverlay(
  47. capStartEdge: false,
  48. background: Colors.transparent,
  49. );
  50. // Lays out the date picker based on how much space each single column needs.
  51. //
  52. // Each column is a child of this delegate, indexed from 0 to number of columns - 1.
  53. // Each column will be padded horizontally by 12.0 both left and right.
  54. //
  55. // The picker will be placed in the center, and the leftmost and rightmost
  56. // column will be extended equally to the remaining width.
  57. class _DatePickerLayoutDelegate extends MultiChildLayoutDelegate {
  58. _DatePickerLayoutDelegate({
  59. required this.columnWidths,
  60. required this.textDirectionFactor,
  61. required this.maxWidth,
  62. });
  63. // The list containing widths of all columns.
  64. final List<double> columnWidths;
  65. // textDirectionFactor is 1 if text is written left to right, and -1 if right to left.
  66. final int textDirectionFactor;
  67. // The max width the children should reach to avoid bending outwards.
  68. final double maxWidth;
  69. @override
  70. void performLayout(Size size) {
  71. double remainingWidth = maxWidth < size.width ? maxWidth : size.width;
  72. double currentHorizontalOffset = (size.width - remainingWidth) / 2;
  73. for (int i = 0; i < columnWidths.length; i++) {
  74. remainingWidth -= columnWidths[i] + _kDatePickerPadSize * 2;
  75. }
  76. for (int i = 0; i < columnWidths.length; i++) {
  77. final int index =
  78. textDirectionFactor == 1 ? i : columnWidths.length - i - 1;
  79. double childWidth = columnWidths[index] + _kDatePickerPadSize * 2;
  80. if (index == 0 || index == columnWidths.length - 1) {
  81. childWidth += remainingWidth / 2;
  82. }
  83. // We can't actually assert here because it would break things badly for
  84. // semantics, which will expect that we laid things out here.
  85. assert(() {
  86. if (childWidth < 0) {
  87. FlutterError.reportError(
  88. FlutterErrorDetails(
  89. exception: FlutterError(
  90. 'Insufficient horizontal space to render the '
  91. 'CupertinoDatePicker because the parent is too narrow at '
  92. '${size.width}px.\n'
  93. 'An additional ${-remainingWidth}px is needed to avoid '
  94. 'overlapping columns.',
  95. ),
  96. ),
  97. );
  98. }
  99. return true;
  100. }());
  101. layoutChild(index,
  102. BoxConstraints.tight(Size(math.max(0.0, childWidth), size.height)));
  103. positionChild(index, Offset(currentHorizontalOffset, 0.0));
  104. currentHorizontalOffset += childWidth;
  105. }
  106. }
  107. @override
  108. bool shouldRelayout(_DatePickerLayoutDelegate oldDelegate) {
  109. return columnWidths != oldDelegate.columnWidths ||
  110. textDirectionFactor != oldDelegate.textDirectionFactor;
  111. }
  112. }
  113. /// Different display modes of [CupertinoDatePicker].
  114. ///
  115. /// See also:
  116. ///
  117. /// * [CupertinoDatePicker], the class that implements different display modes
  118. /// of the iOS-style date picker.
  119. /// * [CupertinoPicker], the class that implements a content agnostic spinner UI.
  120. enum CupertinoDatePickerMode {
  121. /// Mode that shows the date in hour, minute, and (optional) an AM/PM designation.
  122. /// The AM/PM designation is shown only if [CupertinoDatePicker] does not use 24h format.
  123. /// Column order is subject to internationalization.
  124. ///
  125. /// Example: ` 4 | 14 | PM `.
  126. time,
  127. /// Mode that shows the date in month, day of month, and year.
  128. /// Name of month is spelled in full.
  129. /// Column order is subject to internationalization.
  130. ///
  131. /// Example: ` July | 13 | 2012 `.
  132. date,
  133. /// Mode that shows the date as day of the week, month, day of month and
  134. /// the time in hour, minute, and (optional) an AM/PM designation.
  135. /// The AM/PM designation is shown only if [CupertinoDatePicker] does not use 24h format.
  136. /// Column order is subject to internationalization.
  137. ///
  138. /// Example: ` Fri Jul 13 | 4 | 14 | PM `
  139. dateAndTime,
  140. }
  141. // Different types of column in CupertinoDatePicker.
  142. enum _PickerColumnType {
  143. // Day of month column in date mode.
  144. dayOfMonth,
  145. // Month column in date mode.
  146. month,
  147. // Year column in date mode.
  148. year,
  149. }
  150. /// A date picker widget in iOS style.
  151. ///
  152. /// There are several modes of the date picker listed in [CupertinoDatePickerMode].
  153. ///
  154. /// The class will display its children as consecutive columns. Its children
  155. /// order is based on internationalization, or the [dateOrder] property if specified.
  156. ///
  157. /// Example of the picker in date mode:
  158. ///
  159. /// * US-English: `| July | 13 | 2012 |`
  160. /// * Vietnamese: `| 13 | Tháng 7 | 2012 |`
  161. ///
  162. /// Can be used with [showCupertinoModalPopup] to display the picker modally at
  163. /// the bottom of the screen.
  164. ///
  165. /// Sizes itself to its parent and may not render correctly if not given the
  166. /// full screen width. Content texts are shown with
  167. /// [CupertinoTextThemeData.dateTimePickerTextStyle].
  168. ///
  169. /// {@tool dartpad}
  170. /// This sample shows how to implement CupertinoDatePicker with different picker modes.
  171. /// We can provide initial dateTime value for the picker to display. When user changes
  172. /// the drag the date or time wheels, the picker will call onDateTimeChanged callback.
  173. ///
  174. /// CupertinoDatePicker can be displayed directly on a screen or in a popup.
  175. ///
  176. /// ** See code in examples/api/lib/cupertino/date_picker/cupertino_date_picker.0.dart **
  177. /// {@end-tool}
  178. ///
  179. /// See also:
  180. ///
  181. /// * [CupertinoTimerPicker], the class that implements the iOS-style timer picker.
  182. /// * [CupertinoPicker], the class that implements a content agnostic spinner UI.
  183. /// * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/pickers/>
  184. class VCustomCupertinoDatePicker extends StatefulWidget {
  185. /// Constructs an iOS style date picker.
  186. ///
  187. /// [mode] is one of the mode listed in [CupertinoDatePickerMode] and defaults
  188. /// to [CupertinoDatePickerMode.dateAndTime].
  189. ///
  190. /// [onDateTimeChanged] is the callback called when the selected date or time
  191. /// changes and must not be null. When in [CupertinoDatePickerMode.time] mode,
  192. /// the year, month and day will be the same as [initialDateTime]. When in
  193. /// [CupertinoDatePickerMode.date] mode, this callback will always report the
  194. /// start time of the currently selected day.
  195. ///
  196. /// [initialDateTime] is the initial date time of the picker. Defaults to the
  197. /// present date and time and must not be null. The present must conform to
  198. /// the intervals set in [minimumDate], [maximumDate], [minimumYear], and
  199. /// [maximumYear].
  200. ///
  201. /// [minimumDate] is the minimum selectable [DateTime] of the picker. When set
  202. /// to null, the picker does not limit the minimum [DateTime] the user can pick.
  203. /// In [CupertinoDatePickerMode.time] mode, [minimumDate] should typically be
  204. /// on the same date as [initialDateTime], as the picker will not limit the
  205. /// minimum time the user can pick if it's set to a date earlier than that.
  206. ///
  207. /// [maximumDate] is the maximum selectable [DateTime] of the picker. When set
  208. /// to null, the picker does not limit the maximum [DateTime] the user can pick.
  209. /// In [CupertinoDatePickerMode.time] mode, [maximumDate] should typically be
  210. /// on the same date as [initialDateTime], as the picker will not limit the
  211. /// maximum time the user can pick if it's set to a date later than that.
  212. ///
  213. /// [minimumYear] is the minimum year that the picker can be scrolled to in
  214. /// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null.
  215. ///
  216. /// [maximumYear] is the maximum year that the picker can be scrolled to in
  217. /// [CupertinoDatePickerMode.date] mode. Null if there's no limit.
  218. ///
  219. /// [minuteInterval] is the granularity of the minute spinner. Must be a
  220. /// positive integer factor of 60.
  221. ///
  222. /// [use24hFormat] decides whether 24 hour format is used. Defaults to false.
  223. ///
  224. /// [dateOrder] determines the order of the columns inside [CupertinoDatePicker] in date mode.
  225. /// Defaults to the locale's default date format/order.
  226. VCustomCupertinoDatePicker({
  227. super.key,
  228. required this.onDateTimeChanged,
  229. DateTime? initialDateTime,
  230. this.minimumDate,
  231. this.maximumDate,
  232. this.minimumYear = 1,
  233. this.maximumYear,
  234. this.minuteInterval = 1,
  235. this.backgroundColor,
  236. }) : initialDateTime = initialDateTime ?? DateTime.now(),
  237. assert(
  238. minuteInterval > 0 && 60 % minuteInterval == 0,
  239. 'minute interval is not a positive integer factor of 60',
  240. ) {
  241. assert(
  242. this.initialDateTime.minute % minuteInterval == 0,
  243. 'initial minute is not divisible by minute interval',
  244. );
  245. }
  246. /// The initial date and/or time of the picker. Defaults to the present date
  247. /// and time and must not be null. The present must conform to the intervals
  248. /// set in [minimumDate], [maximumDate], [minimumYear], and [maximumYear].
  249. ///
  250. /// Changing this value after the initial build will not affect the currently
  251. /// selected date time.
  252. final DateTime initialDateTime;
  253. /// The minimum selectable date that the picker can settle on.
  254. ///
  255. /// When non-null, the user can still scroll the picker to [DateTime]s earlier
  256. /// than [minimumDate], but the [onDateTimeChanged] will not be called on
  257. /// these [DateTime]s. Once let go, the picker will scroll back to [minimumDate].
  258. ///
  259. /// In [CupertinoDatePickerMode.time] mode, a time becomes unselectable if the
  260. /// [DateTime] produced by combining that particular time and the date part of
  261. /// [initialDateTime] is earlier than [minimumDate]. So typically [minimumDate]
  262. /// needs to be set to a [DateTime] that is on the same date as [initialDateTime].
  263. ///
  264. /// Defaults to null. When set to null, the picker does not impose a limit on
  265. /// the earliest [DateTime] the user can select.
  266. final DateTime? minimumDate;
  267. /// The maximum selectable date that the picker can settle on.
  268. ///
  269. /// When non-null, the user can still scroll the picker to [DateTime]s later
  270. /// than [maximumDate], but the [onDateTimeChanged] will not be called on
  271. /// these [DateTime]s. Once let go, the picker will scroll back to [maximumDate].
  272. ///
  273. /// In [CupertinoDatePickerMode.time] mode, a time becomes unselectable if the
  274. /// [DateTime] produced by combining that particular time and the date part of
  275. /// [initialDateTime] is later than [maximumDate]. So typically [maximumDate]
  276. /// needs to be set to a [DateTime] that is on the same date as [initialDateTime].
  277. ///
  278. /// Defaults to null. When set to null, the picker does not impose a limit on
  279. /// the latest [DateTime] the user can select.
  280. final DateTime? maximumDate;
  281. /// Minimum year that the picker can be scrolled to in
  282. /// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null.
  283. final int minimumYear;
  284. /// Maximum year that the picker can be scrolled to in
  285. /// [CupertinoDatePickerMode.date] mode. Null if there's no limit.
  286. final int? maximumYear;
  287. /// The granularity of the minutes spinner, if it is shown in the current mode.
  288. /// Must be an integer factor of 60.
  289. final int minuteInterval;
  290. /// Callback called when the selected date and/or time changes. If the new
  291. /// selected [DateTime] is not valid, or is not in the [minimumDate] through
  292. /// [maximumDate] range, this callback will not be called.
  293. ///
  294. /// Must not be null.
  295. final ValueChanged<DateTime> onDateTimeChanged;
  296. /// Background color of date picker.
  297. ///
  298. /// Defaults to null, which disables background painting entirely.
  299. final Color? backgroundColor;
  300. @override
  301. State<StatefulWidget> createState() =>
  302. // ignore: no_logic_in_create_state
  303. _CupertinoDatePickerDateState();
  304. // Estimate the minimum width that each column needs to layout its content.
  305. static double _getColumnWidth(
  306. _PickerColumnType columnType, BuildContext context) {
  307. switch (columnType) {
  308. case _PickerColumnType.year:
  309. return 132;
  310. case _PickerColumnType.month:
  311. return 90;
  312. case _PickerColumnType.dayOfMonth:
  313. return 110;
  314. default:
  315. return 0;
  316. }
  317. }
  318. }
  319. typedef _ColumnBuilder = Widget Function(double offAxisFraction,
  320. TransitionBuilder itemPositioningBuilder, Widget selectionOverlay);
  321. class _CupertinoDatePickerDateState extends State<VCustomCupertinoDatePicker> {
  322. late int textDirectionFactor;
  323. // Alignment based on text direction. The variable name is self descriptive,
  324. // however, when text direction is rtl, alignment is reversed.
  325. late Alignment alignCenterLeft;
  326. late Alignment alignCenterRight;
  327. // The currently selected values of the picker.
  328. late int selectedDay;
  329. late int selectedMonth;
  330. late int selectedYear;
  331. // The controller of the day picker. There are cases where the selected value
  332. // of the picker is invalid (e.g. February 30th 2018), and this dayController
  333. // is responsible for jumping to a valid value.
  334. late FixedExtentScrollController dayController;
  335. late FixedExtentScrollController monthController;
  336. late FixedExtentScrollController yearController;
  337. bool isDayPickerScrolling = false;
  338. bool isMonthPickerScrolling = false;
  339. bool isYearPickerScrolling = false;
  340. bool get isScrolling =>
  341. isDayPickerScrolling || isMonthPickerScrolling || isYearPickerScrolling;
  342. // Estimated width of columns.
  343. Map<int, double> estimatedColumnWidths = <int, double>{};
  344. @override
  345. void initState() {
  346. super.initState();
  347. selectedDay = widget.initialDateTime.day;
  348. selectedMonth = widget.initialDateTime.month;
  349. selectedYear = widget.initialDateTime.year;
  350. dayController = FixedExtentScrollController(initialItem: selectedDay - 1);
  351. monthController =
  352. FixedExtentScrollController(initialItem: selectedMonth - 1);
  353. yearController = FixedExtentScrollController(initialItem: selectedYear);
  354. PaintingBinding.instance.systemFonts.addListener(_handleSystemFontsChange);
  355. }
  356. void _handleSystemFontsChange() {
  357. setState(() {
  358. // System fonts change might cause the text layout width to change.
  359. _refreshEstimatedColumnWidths();
  360. });
  361. }
  362. @override
  363. void dispose() {
  364. dayController.dispose();
  365. monthController.dispose();
  366. yearController.dispose();
  367. PaintingBinding.instance.systemFonts
  368. .removeListener(_handleSystemFontsChange);
  369. super.dispose();
  370. }
  371. @override
  372. void didChangeDependencies() {
  373. super.didChangeDependencies();
  374. textDirectionFactor =
  375. Directionality.of(context) == TextDirection.ltr ? 1 : -1;
  376. alignCenterLeft =
  377. textDirectionFactor == 1 ? Alignment.centerLeft : Alignment.centerRight;
  378. alignCenterRight =
  379. textDirectionFactor == 1 ? Alignment.centerRight : Alignment.centerLeft;
  380. _refreshEstimatedColumnWidths();
  381. }
  382. void _refreshEstimatedColumnWidths() {
  383. estimatedColumnWidths[_PickerColumnType.dayOfMonth.index] =
  384. VCustomCupertinoDatePicker._getColumnWidth(
  385. _PickerColumnType.dayOfMonth, context);
  386. estimatedColumnWidths[_PickerColumnType.month.index] =
  387. VCustomCupertinoDatePicker._getColumnWidth(
  388. _PickerColumnType.month, context);
  389. estimatedColumnWidths[_PickerColumnType.year.index] =
  390. VCustomCupertinoDatePicker._getColumnWidth(
  391. _PickerColumnType.year, context);
  392. }
  393. // The DateTime of the last day of a given month in a given year.
  394. // Let `DateTime` handle the year/month overflow.
  395. DateTime _lastDayInMonth(int year, int month) => DateTime(year, month + 1, 0);
  396. Widget _buildDayPicker(double offAxisFraction,
  397. TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
  398. final int daysInCurrentMonth =
  399. _lastDayInMonth(selectedYear, selectedMonth).day;
  400. return NotificationListener<ScrollNotification>(
  401. onNotification: (ScrollNotification notification) {
  402. if (notification is ScrollStartNotification) {
  403. isDayPickerScrolling = true;
  404. } else if (notification is ScrollEndNotification) {
  405. isDayPickerScrolling = false;
  406. _pickerDidStopScrolling();
  407. }
  408. return false;
  409. },
  410. child: CupertinoPicker(
  411. scrollController: dayController,
  412. // offAxisFraction: offAxisFraction,
  413. itemExtent: _kItemExtent,
  414. useMagnifier: _kUseMagnifier,
  415. magnification: _kMagnification,
  416. backgroundColor: widget.backgroundColor,
  417. squeeze: _kSqueeze,
  418. onSelectedItemChanged: (int index) {
  419. selectedDay = index + 1;
  420. if (_isCurrentDateValid) {
  421. widget.onDateTimeChanged(
  422. DateTime(selectedYear, selectedMonth, selectedDay));
  423. }
  424. },
  425. looping: true,
  426. selectionOverlay: selectionOverlay,
  427. children: List<Widget>.generate(31, (int index) {
  428. final int day = index + 1;
  429. return itemPositioningBuilder(
  430. context,
  431. Text(
  432. "$day日",
  433. style:
  434. _themeTextStyle(context, isValid: day <= daysInCurrentMonth),
  435. ),
  436. );
  437. }),
  438. ),
  439. );
  440. }
  441. Widget _buildMonthPicker(double offAxisFraction,
  442. TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
  443. return NotificationListener<ScrollNotification>(
  444. onNotification: (ScrollNotification notification) {
  445. if (notification is ScrollStartNotification) {
  446. isMonthPickerScrolling = true;
  447. } else if (notification is ScrollEndNotification) {
  448. isMonthPickerScrolling = false;
  449. _pickerDidStopScrolling();
  450. }
  451. return false;
  452. },
  453. child: CupertinoPicker(
  454. scrollController: monthController,
  455. // offAxisFraction: offAxisFraction,
  456. itemExtent: _kItemExtent,
  457. useMagnifier: _kUseMagnifier,
  458. magnification: _kMagnification,
  459. backgroundColor: widget.backgroundColor,
  460. squeeze: _kSqueeze,
  461. onSelectedItemChanged: (int index) {
  462. selectedMonth = index + 1;
  463. if (_isCurrentDateValid) {
  464. widget.onDateTimeChanged(
  465. DateTime(selectedYear, selectedMonth, selectedDay));
  466. }
  467. },
  468. looping: true,
  469. selectionOverlay: selectionOverlay,
  470. children: List<Widget>.generate(12, (int index) {
  471. final int month = index + 1;
  472. final bool isInvalidMonth =
  473. (widget.minimumDate?.year == selectedYear &&
  474. widget.minimumDate!.month > month) ||
  475. (widget.maximumDate?.year == selectedYear &&
  476. widget.maximumDate!.month < month);
  477. return itemPositioningBuilder(
  478. context,
  479. Text(
  480. "$month月",
  481. style: _themeTextStyle(context, isValid: !isInvalidMonth),
  482. ),
  483. );
  484. }),
  485. ),
  486. );
  487. }
  488. Widget _buildYearPicker(double offAxisFraction,
  489. TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
  490. return NotificationListener<ScrollNotification>(
  491. onNotification: (ScrollNotification notification) {
  492. if (notification is ScrollStartNotification) {
  493. isYearPickerScrolling = true;
  494. } else if (notification is ScrollEndNotification) {
  495. isYearPickerScrolling = false;
  496. _pickerDidStopScrolling();
  497. }
  498. return false;
  499. },
  500. child: CupertinoPicker.builder(
  501. scrollController: yearController,
  502. itemExtent: _kItemExtent,
  503. // offAxisFraction: offAxisFraction,
  504. useMagnifier: _kUseMagnifier,
  505. magnification: _kMagnification,
  506. backgroundColor: widget.backgroundColor,
  507. onSelectedItemChanged: (int index) {
  508. selectedYear = index;
  509. if (_isCurrentDateValid) {
  510. widget.onDateTimeChanged(
  511. DateTime(selectedYear, selectedMonth, selectedDay));
  512. }
  513. },
  514. itemBuilder: (BuildContext context, int year) {
  515. if (year < widget.minimumYear) {
  516. return null;
  517. }
  518. if (widget.maximumYear != null && year > widget.maximumYear!) {
  519. return null;
  520. }
  521. final bool isValidYear = (widget.minimumDate == null ||
  522. widget.minimumDate!.year <= year) &&
  523. (widget.maximumDate == null || widget.maximumDate!.year >= year);
  524. return itemPositioningBuilder(
  525. context,
  526. Text(
  527. "$year年",
  528. style: _themeTextStyle(context, isValid: isValidYear),
  529. ),
  530. );
  531. },
  532. selectionOverlay: selectionOverlay,
  533. ),
  534. );
  535. }
  536. bool get _isCurrentDateValid {
  537. // The current date selection represents a range [minSelectedData, maxSelectDate].
  538. final DateTime minSelectedDate =
  539. DateTime(selectedYear, selectedMonth, selectedDay);
  540. final DateTime maxSelectedDate =
  541. DateTime(selectedYear, selectedMonth, selectedDay + 1);
  542. final bool minCheck = widget.minimumDate?.isBefore(maxSelectedDate) ?? true;
  543. final bool maxCheck =
  544. widget.maximumDate?.isBefore(minSelectedDate) ?? false;
  545. return minCheck && !maxCheck && minSelectedDate.day == selectedDay;
  546. }
  547. // One or more pickers have just stopped scrolling.
  548. void _pickerDidStopScrolling() {
  549. // Call setState to update the greyed out days/months/years, as the currently
  550. // selected year/month may have changed.
  551. setState(() {});
  552. if (isScrolling) {
  553. return;
  554. }
  555. // Whenever scrolling lands on an invalid entry, the picker
  556. // automatically scrolls to a valid one.
  557. final DateTime minSelectDate =
  558. DateTime(selectedYear, selectedMonth, selectedDay);
  559. final DateTime maxSelectDate =
  560. DateTime(selectedYear, selectedMonth, selectedDay + 1);
  561. final bool minCheck = widget.minimumDate?.isBefore(maxSelectDate) ?? true;
  562. final bool maxCheck = widget.maximumDate?.isBefore(minSelectDate) ?? false;
  563. if (!minCheck || maxCheck) {
  564. // We have minCheck === !maxCheck.
  565. final DateTime targetDate =
  566. minCheck ? widget.maximumDate! : widget.minimumDate!;
  567. _scrollToDate(targetDate);
  568. return;
  569. }
  570. // Some months have less days (e.g. February). Go to the last day of that month
  571. // if the selectedDay exceeds the maximum.
  572. if (minSelectDate.day != selectedDay) {
  573. final DateTime lastDay = _lastDayInMonth(selectedYear, selectedMonth);
  574. _scrollToDate(lastDay);
  575. }
  576. }
  577. void _scrollToDate(DateTime newDate) {
  578. SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
  579. if (selectedYear != newDate.year) {
  580. _animateColumnControllerToItem(yearController, newDate.year);
  581. }
  582. if (selectedMonth != newDate.month) {
  583. _animateColumnControllerToItem(monthController, newDate.month - 1);
  584. }
  585. if (selectedDay != newDate.day) {
  586. _animateColumnControllerToItem(dayController, newDate.day - 1);
  587. }
  588. });
  589. }
  590. @override
  591. Widget build(BuildContext context) {
  592. final List<_ColumnBuilder> pickerBuilders = <_ColumnBuilder>[
  593. _buildYearPicker,
  594. _buildMonthPicker,
  595. _buildDayPicker,
  596. ];
  597. final List<double> columnWidths = <double>[
  598. estimatedColumnWidths[_PickerColumnType.year.index]!,
  599. estimatedColumnWidths[_PickerColumnType.month.index]!,
  600. estimatedColumnWidths[_PickerColumnType.dayOfMonth.index]!,
  601. ];
  602. final List<Widget> pickers = <Widget>[];
  603. double totalColumnWidths = 4 * _kDatePickerPadSize;
  604. for (int i = 0; i < columnWidths.length; i++) {
  605. final double offAxisFraction = (i - 1) * 0.3 * textDirectionFactor;
  606. EdgeInsets padding = const EdgeInsets.only(right: _kDatePickerPadSize);
  607. if (textDirectionFactor == -1) {
  608. padding = const EdgeInsets.only(left: _kDatePickerPadSize);
  609. }
  610. Widget selectionOverlay = _centerSelectionOverlay;
  611. if (i == 0) {
  612. selectionOverlay = _startSelectionOverlay;
  613. } else if (i == columnWidths.length - 1) {
  614. selectionOverlay = _endSelectionOverlay;
  615. }
  616. totalColumnWidths += columnWidths[i] + (2 * _kDatePickerPadSize);
  617. pickers.add(LayoutId(
  618. id: i,
  619. child: pickerBuilders[i](
  620. offAxisFraction,
  621. (BuildContext context, Widget? child) {
  622. return Container(
  623. alignment: i == columnWidths.length - 1
  624. ? alignCenterLeft
  625. : alignCenterRight,
  626. padding: i == 0 ? null : padding,
  627. child: Container(
  628. alignment: i == 0 ? alignCenterLeft : alignCenterRight,
  629. width: columnWidths[i] + _kDatePickerPadSize,
  630. child: child,
  631. ),
  632. );
  633. },
  634. selectionOverlay,
  635. ),
  636. ));
  637. }
  638. final double maxPickerWidth =
  639. totalColumnWidths > _kPickerWidth ? totalColumnWidths : _kPickerWidth;
  640. return MediaQuery(
  641. data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
  642. child: DefaultTextStyle.merge(
  643. style: _kDefaultPickerTextStyle,
  644. child: CustomMultiChildLayout(
  645. delegate: _DatePickerLayoutDelegate(
  646. columnWidths: columnWidths,
  647. textDirectionFactor: textDirectionFactor,
  648. maxWidth: maxPickerWidth,
  649. ),
  650. children: pickers,
  651. ),
  652. ),
  653. );
  654. }
  655. }