flutter_datetime_picker.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. library flutter_datetime_picker;
  2. import 'package:flutter/cupertino.dart';
  3. import 'package:flutter/material.dart';
  4. typedef DateChangedCallback(int year, int month, int date);
  5. const double _kDatePickerHeight = 210.0;
  6. const double _kDatePickerTitleHeight = 44.0;
  7. const double _kDatePickerItemHeight = 36.0;
  8. const double _kDatePickerFontSize = 18.0;
  9. const int _kDefaultMinYear = 1970;
  10. const int _kDefaultMaxYear = 2050;
  11. const List<String> monthNames = const <String>[
  12. '1',
  13. '2',
  14. '3',
  15. '4',
  16. '5',
  17. '6',
  18. '7',
  19. '8',
  20. '9',
  21. '10',
  22. '11',
  23. '12',
  24. ];
  25. const List<int> leapYearMonths = const <int>[1, 3, 5, 7, 8, 10, 12];
  26. class DatePicker {
  27. ///
  28. /// Display date picker bottom sheet.
  29. ///
  30. static void showDatePicker(BuildContext context,
  31. {bool showTitleActions: true,
  32. int minYear: _kDefaultMinYear,
  33. int maxYear: _kDefaultMaxYear,
  34. int initialYear: _kDefaultMinYear,
  35. int initialMonth: 1,
  36. int initialDate: 1,
  37. DateChangedCallback onChanged,
  38. DateChangedCallback onConfirm,
  39. locale: 'en_NZ'}) {
  40. Navigator.push(
  41. context,
  42. new _DatePickerRoute(
  43. showTitleActions: showTitleActions,
  44. minYear: minYear,
  45. maxYear: maxYear,
  46. initialYear: initialYear,
  47. initialMonth: initialMonth,
  48. initialDate: initialDate,
  49. onChanged: onChanged,
  50. onConfirm: onConfirm,
  51. locale: locale,
  52. theme: Theme.of(context, shadowThemeOnly: true),
  53. barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
  54. ));
  55. }
  56. }
  57. class _DatePickerRoute<T> extends PopupRoute<T> {
  58. _DatePickerRoute({
  59. this.showTitleActions,
  60. this.minYear,
  61. this.maxYear,
  62. this.initialYear,
  63. this.initialMonth,
  64. this.initialDate,
  65. this.onChanged,
  66. this.onConfirm,
  67. this.theme,
  68. this.barrierLabel,
  69. this.locale,
  70. RouteSettings settings,
  71. }) : super(settings: settings);
  72. final bool showTitleActions;
  73. final int minYear, maxYear, initialYear, initialMonth, initialDate;
  74. final DateChangedCallback onChanged;
  75. final DateChangedCallback onConfirm;
  76. final ThemeData theme;
  77. final String locale;
  78. @override
  79. Duration get transitionDuration => const Duration(milliseconds: 200);
  80. @override
  81. bool get barrierDismissible => true;
  82. @override
  83. final String barrierLabel;
  84. @override
  85. Color get barrierColor => Colors.black54;
  86. AnimationController _animationController;
  87. @override
  88. AnimationController createAnimationController() {
  89. assert(_animationController == null);
  90. _animationController = BottomSheet.createAnimationController(navigator.overlay);
  91. return _animationController;
  92. }
  93. @override
  94. Widget buildPage(
  95. BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
  96. Widget bottomSheet = new MediaQuery.removePadding(
  97. context: context,
  98. removeTop: true,
  99. child: _DatePickerComponent(
  100. minYear: minYear,
  101. maxYear: maxYear,
  102. initialYear: initialYear,
  103. initialMonth: initialMonth,
  104. initialDay: initialDate,
  105. onChanged: onChanged,
  106. locale: this.locale,
  107. route: this,
  108. ),
  109. );
  110. if (theme != null) {
  111. bottomSheet = new Theme(data: theme, child: bottomSheet);
  112. }
  113. return bottomSheet;
  114. }
  115. }
  116. class _DatePickerComponent extends StatefulWidget {
  117. _DatePickerComponent(
  118. {Key key,
  119. @required this.route,
  120. this.minYear: _kDefaultMinYear,
  121. this.maxYear: _kDefaultMaxYear,
  122. this.initialYear: -1,
  123. this.initialMonth: 1,
  124. this.initialDay: 1,
  125. this.onChanged,
  126. this.locale});
  127. final DateChangedCallback onChanged;
  128. final int minYear, maxYear, initialYear, initialMonth, initialDay;
  129. final _DatePickerRoute route;
  130. final String locale;
  131. @override
  132. State<StatefulWidget> createState() => _DatePickerState(
  133. this.minYear, this.maxYear, this.initialYear, this.initialMonth, this.initialDay);
  134. }
  135. class _DatePickerState extends State<_DatePickerComponent> {
  136. final int minYear, maxYear;
  137. int _currentYear, _currentMonth, _currentDay;
  138. int _dayCountOfMonth;
  139. FixedExtentScrollController leftScrollCtrl, middleScrollCtrl, rightScrollCtrl;
  140. _DatePickerState(
  141. this.minYear, this.maxYear, this._currentYear, this._currentMonth, this._currentDay) {
  142. if (this._currentYear == -1) {
  143. this._currentYear = this.minYear;
  144. }
  145. if (this._currentYear < this.minYear) {
  146. this._currentYear = this.minYear;
  147. }
  148. if (this._currentYear > this.maxYear) {
  149. this._currentYear = this.maxYear;
  150. }
  151. if (this._currentMonth < 1) {
  152. this._currentMonth = 1;
  153. }
  154. if (this._currentMonth > 12) {
  155. this._currentMonth = 12;
  156. }
  157. if (this._currentDay < 1) {
  158. this._currentDay = 1;
  159. }
  160. if (this._currentDay > 31) {
  161. this._currentDay = 31;
  162. }
  163. leftScrollCtrl = new FixedExtentScrollController(initialItem: _currentYear - this.minYear);
  164. middleScrollCtrl = new FixedExtentScrollController(initialItem: _currentMonth - 1);
  165. rightScrollCtrl = new FixedExtentScrollController(initialItem: _currentDay - 1);
  166. _dayCountOfMonth = _calcDateCount();
  167. }
  168. @override
  169. Widget build(BuildContext context) {
  170. return new GestureDetector(
  171. child: new AnimatedBuilder(
  172. animation: widget.route.animation,
  173. builder: (BuildContext context, Widget child) {
  174. return new ClipRect(
  175. child: new CustomSingleChildLayout(
  176. delegate: new _BottomPickerLayout(widget.route.animation.value,
  177. showTitleActions: widget.route.showTitleActions),
  178. child: new GestureDetector(
  179. child: Material(
  180. color: Colors.transparent,
  181. child: _renderPickerView(),
  182. ),
  183. ),
  184. ),
  185. );
  186. },
  187. ),
  188. );
  189. }
  190. void _setYear(int index) {
  191. int year = widget.minYear + index;
  192. if (_currentYear != year) {
  193. _currentYear = year;
  194. _notifyDateChanged();
  195. }
  196. }
  197. void _setMonth(int index) {
  198. int month = index + 1;
  199. if (_currentMonth != month) {
  200. _currentMonth = month;
  201. int dateCount = _calcDateCount();
  202. if (_dayCountOfMonth != dateCount) {
  203. setState(() {
  204. _dayCountOfMonth = dateCount;
  205. });
  206. }
  207. if (_currentDay > dateCount) {
  208. _currentDay = dateCount;
  209. }
  210. _notifyDateChanged();
  211. }
  212. }
  213. void _setDay(int index) {
  214. int date = index + 1;
  215. if (_currentDay != date) {
  216. _currentDay = date;
  217. _notifyDateChanged();
  218. }
  219. }
  220. int _calcDateCount() {
  221. if (leapYearMonths.contains(_currentMonth)) {
  222. return 31;
  223. } else if (_currentMonth == 2) {
  224. if ((_currentYear % 4 == 0 && _currentYear % 100 != 0) || _currentYear % 400 == 0) {
  225. return 29;
  226. }
  227. return 28;
  228. }
  229. return 30;
  230. }
  231. List<String> _yearList() {
  232. return List.generate(widget.maxYear - widget.minYear + 1, (int index) {
  233. return '${widget.minYear + index}${_localeYear()}';
  234. });
  235. }
  236. List<String> _monthList() {
  237. return monthNames.map((string) => '$string${_localeMonth()}').toList();
  238. }
  239. List<String> _dayList() {
  240. return List.generate(_dayCountOfMonth, (int index) {
  241. return '${index + 1}${_localeDay()}';
  242. });
  243. ;
  244. }
  245. void _notifyDateChanged() {
  246. if (widget.onChanged != null) {
  247. widget.onChanged(_currentYear, _currentMonth, _currentDay);
  248. }
  249. }
  250. Widget _renderPickerView() {
  251. Widget itemView = _renderItemView();
  252. if (widget.route.showTitleActions) {
  253. return Column(
  254. children: <Widget>[
  255. _renderTitleActionsView(),
  256. itemView,
  257. ],
  258. );
  259. }
  260. return itemView;
  261. }
  262. Widget _renderColumnView(
  263. List<String> lists, ScrollController scrollController, ValueChanged<int> selectedChanged) {
  264. return Expanded(
  265. flex: 1,
  266. child: Container(
  267. padding: EdgeInsets.all(8.0),
  268. height: _kDatePickerHeight,
  269. decoration: BoxDecoration(color: Colors.white),
  270. child: CupertinoPicker.builder(
  271. backgroundColor: Colors.white,
  272. scrollController: scrollController,
  273. itemExtent: _kDatePickerItemHeight,
  274. onSelectedItemChanged: (int index) {
  275. selectedChanged(index);
  276. },
  277. useMagnifier: true,
  278. itemBuilder: (BuildContext context, int index) {
  279. if (index < lists.length && index >= 0) {
  280. return Container(
  281. height: _kDatePickerItemHeight,
  282. alignment: Alignment.center,
  283. child: Text(
  284. lists[index],
  285. style: TextStyle(color: Color(0xFF000046), fontSize: _kDatePickerFontSize),
  286. textAlign: TextAlign.start,
  287. ),
  288. );
  289. } else {
  290. return null;
  291. }
  292. })),
  293. );
  294. }
  295. Widget _renderItemView() {
  296. return Row(
  297. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  298. children: <Widget>[
  299. _renderColumnView(_yearList(), leftScrollCtrl, (year) {
  300. _setYear(year);
  301. }),
  302. _renderColumnView(_monthList(), middleScrollCtrl, (month) {
  303. _setMonth(month);
  304. }),
  305. _renderColumnView(_dayList(), rightScrollCtrl, (day) {
  306. _setDay(day);
  307. }),
  308. ],
  309. );
  310. }
  311. // Title View
  312. Widget _renderTitleActionsView() {
  313. String done = _localeDone();
  314. String cancel = _localeCancel();
  315. return Container(
  316. height: _kDatePickerTitleHeight,
  317. decoration: BoxDecoration(color: Colors.white),
  318. child: Row(
  319. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  320. children: <Widget>[
  321. Container(
  322. height: _kDatePickerTitleHeight,
  323. child: FlatButton(
  324. child: Text(
  325. '$cancel',
  326. style: TextStyle(
  327. color: Theme.of(context).unselectedWidgetColor,
  328. fontSize: 16.0,
  329. ),
  330. ),
  331. onPressed: () => Navigator.pop(context),
  332. ),
  333. ),
  334. Container(
  335. height: _kDatePickerTitleHeight,
  336. child: FlatButton(
  337. child: Text(
  338. '$done',
  339. style: TextStyle(
  340. color: Theme.of(context).primaryColor,
  341. fontSize: 16.0,
  342. ),
  343. ),
  344. onPressed: () {
  345. if (widget.route.onConfirm != null) {
  346. widget.route.onConfirm(_currentYear, _currentMonth, _currentDay);
  347. }
  348. Navigator.pop(context);
  349. },
  350. ),
  351. ),
  352. ],
  353. ),
  354. );
  355. }
  356. String _localeDone() {
  357. if (widget.locale == null) {
  358. return 'Done';
  359. }
  360. String lang = widget.locale.split('_').first;
  361. switch (lang) {
  362. case 'en':
  363. return 'Done';
  364. break;
  365. case 'zh':
  366. return '确定';
  367. break;
  368. default:
  369. return '';
  370. break;
  371. }
  372. }
  373. String _localeCancel() {
  374. if (widget.locale == null) {
  375. return 'Cancel';
  376. }
  377. String lang = widget.locale.split('_').first;
  378. switch (lang) {
  379. case 'en':
  380. return 'Cancel';
  381. break;
  382. case 'zh':
  383. return '取消';
  384. break;
  385. default:
  386. return '';
  387. break;
  388. }
  389. }
  390. String _localeYear() {
  391. if (widget.locale == null) {
  392. return '';
  393. }
  394. String lang = widget.locale.split('_').first;
  395. switch (lang) {
  396. case 'zh':
  397. return '年';
  398. break;
  399. default:
  400. return '';
  401. break;
  402. }
  403. }
  404. String _localeMonth() {
  405. if (widget.locale == null) {
  406. return '';
  407. }
  408. String lang = widget.locale.split('_').first;
  409. switch (lang) {
  410. case 'zh':
  411. return '月';
  412. break;
  413. default:
  414. return '';
  415. break;
  416. }
  417. }
  418. String _localeDay() {
  419. if (widget.locale == null) {
  420. return '';
  421. }
  422. String lang = widget.locale.split('_').first;
  423. switch (lang) {
  424. case 'zh':
  425. return '日';
  426. break;
  427. default:
  428. return '';
  429. break;
  430. }
  431. }
  432. }
  433. class _BottomPickerLayout extends SingleChildLayoutDelegate {
  434. _BottomPickerLayout(this.progress, {this.itemCount, this.showTitleActions});
  435. final double progress;
  436. final int itemCount;
  437. final bool showTitleActions;
  438. @override
  439. BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
  440. double maxHeight = _kDatePickerHeight;
  441. if (showTitleActions) {
  442. maxHeight += _kDatePickerTitleHeight;
  443. }
  444. return new BoxConstraints(
  445. minWidth: constraints.maxWidth,
  446. maxWidth: constraints.maxWidth,
  447. minHeight: 0.0,
  448. maxHeight: maxHeight);
  449. }
  450. @override
  451. Offset getPositionForChild(Size size, Size childSize) {
  452. double height = size.height - childSize.height * progress;
  453. return new Offset(0.0, height);
  454. }
  455. @override
  456. bool shouldRelayout(_BottomPickerLayout oldDelegate) {
  457. return progress != oldDelegate.progress;
  458. }
  459. }