flutter_datetime_picker.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. library flutter_datetime_picker;
  2. import 'dart:async';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_datetime_picker/src/datetime_picker_theme.dart';
  6. import 'package:flutter_datetime_picker/src/date_model.dart';
  7. import 'package:flutter_datetime_picker/src/i18n_model.dart';
  8. export 'package:flutter_datetime_picker/src/datetime_picker_theme.dart';
  9. export 'package:flutter_datetime_picker/src/date_model.dart';
  10. export 'package:flutter_datetime_picker/src/i18n_model.dart';
  11. typedef DateChangedCallback(DateTime time);
  12. typedef DateCancelledCallback();
  13. typedef String StringAtIndexCallBack(int index);
  14. class DatePicker {
  15. ///
  16. /// Display date picker bottom sheet.
  17. ///
  18. static Future<DateTime> showDatePicker(
  19. BuildContext context, {
  20. bool showTitleActions: true,
  21. DateTime minTime,
  22. DateTime maxTime,
  23. DateChangedCallback onChanged,
  24. DateChangedCallback onConfirm,
  25. DateCancelledCallback onCancel,
  26. locale: LocaleType.en,
  27. DateTime currentTime,
  28. DatePickerTheme theme,
  29. }) async {
  30. return await Navigator.push(
  31. context,
  32. _DatePickerRoute(
  33. showTitleActions: showTitleActions,
  34. onChanged: onChanged,
  35. onConfirm: onConfirm,
  36. onCancel: onCancel,
  37. locale: locale,
  38. theme: theme,
  39. barrierLabel:
  40. MaterialLocalizations.of(context).modalBarrierDismissLabel,
  41. pickerModel: DatePickerModel(
  42. currentTime: currentTime,
  43. maxTime: maxTime,
  44. minTime: minTime,
  45. locale: locale,
  46. ),
  47. ),
  48. );
  49. }
  50. ///
  51. /// Display time picker bottom sheet.
  52. ///
  53. static Future<DateTime> showTimePicker(
  54. BuildContext context, {
  55. bool showTitleActions: true,
  56. bool showSecondsColumn: true,
  57. DateChangedCallback onChanged,
  58. DateChangedCallback onConfirm,
  59. DateCancelledCallback onCancel,
  60. locale: LocaleType.en,
  61. DateTime currentTime,
  62. DatePickerTheme theme,
  63. }) async {
  64. return await Navigator.push(
  65. context,
  66. _DatePickerRoute(
  67. showTitleActions: showTitleActions,
  68. onChanged: onChanged,
  69. onConfirm: onConfirm,
  70. onCancel: onCancel,
  71. locale: locale,
  72. theme: theme,
  73. barrierLabel:
  74. MaterialLocalizations.of(context).modalBarrierDismissLabel,
  75. pickerModel: TimePickerModel(
  76. currentTime: currentTime,
  77. locale: locale,
  78. showSecondsColumn: showSecondsColumn,
  79. ),
  80. ),
  81. );
  82. }
  83. ///
  84. /// Display time picker bottom sheet with AM/PM.
  85. ///
  86. static Future<DateTime> showTime12hPicker(
  87. BuildContext context, {
  88. bool showTitleActions: true,
  89. DateChangedCallback onChanged,
  90. DateChangedCallback onConfirm,
  91. DateCancelledCallback onCancel,
  92. locale: LocaleType.en,
  93. DateTime currentTime,
  94. DatePickerTheme theme,
  95. }) async {
  96. return await Navigator.push(
  97. context,
  98. _DatePickerRoute(
  99. showTitleActions: showTitleActions,
  100. onChanged: onChanged,
  101. onConfirm: onConfirm,
  102. onCancel: onCancel,
  103. locale: locale,
  104. theme: theme,
  105. barrierLabel:
  106. MaterialLocalizations.of(context).modalBarrierDismissLabel,
  107. pickerModel: Time12hPickerModel(
  108. currentTime: currentTime,
  109. locale: locale,
  110. ),
  111. ),
  112. );
  113. }
  114. ///
  115. /// Display date&time picker bottom sheet.
  116. ///
  117. static Future<DateTime> showDateTimePicker(
  118. BuildContext context, {
  119. bool showTitleActions: true,
  120. DateTime minTime,
  121. DateTime maxTime,
  122. DateChangedCallback onChanged,
  123. DateChangedCallback onConfirm,
  124. DateCancelledCallback onCancel,
  125. locale: LocaleType.en,
  126. DateTime currentTime,
  127. DatePickerTheme theme,
  128. }) async {
  129. return await Navigator.push(
  130. context,
  131. _DatePickerRoute(
  132. showTitleActions: showTitleActions,
  133. onChanged: onChanged,
  134. onConfirm: onConfirm,
  135. onCancel: onCancel,
  136. locale: locale,
  137. theme: theme,
  138. barrierLabel:
  139. MaterialLocalizations.of(context).modalBarrierDismissLabel,
  140. pickerModel: DateTimePickerModel(
  141. currentTime: currentTime,
  142. minTime: minTime,
  143. maxTime: maxTime,
  144. locale: locale,
  145. ),
  146. ),
  147. );
  148. }
  149. ///
  150. /// Display date picker bottom sheet witch custom picker model.
  151. ///
  152. static Future<DateTime> showPicker(
  153. BuildContext context, {
  154. bool showTitleActions: true,
  155. DateChangedCallback onChanged,
  156. DateChangedCallback onConfirm,
  157. DateCancelledCallback onCancel,
  158. locale: LocaleType.en,
  159. BasePickerModel pickerModel,
  160. DatePickerTheme theme,
  161. }) async {
  162. return await Navigator.push(
  163. context,
  164. _DatePickerRoute(
  165. showTitleActions: showTitleActions,
  166. onChanged: onChanged,
  167. onConfirm: onConfirm,
  168. onCancel: onCancel,
  169. locale: locale,
  170. theme: theme,
  171. barrierLabel:
  172. MaterialLocalizations.of(context).modalBarrierDismissLabel,
  173. pickerModel: pickerModel,
  174. ),
  175. );
  176. }
  177. }
  178. class _DatePickerRoute<T> extends PopupRoute<T> {
  179. _DatePickerRoute({
  180. this.showTitleActions,
  181. this.onChanged,
  182. this.onConfirm,
  183. this.onCancel,
  184. theme,
  185. this.barrierLabel,
  186. this.locale,
  187. RouteSettings settings,
  188. pickerModel,
  189. }) : this.pickerModel = pickerModel ?? DatePickerModel(),
  190. this.theme = theme ?? DatePickerTheme(),
  191. super(settings: settings);
  192. final bool showTitleActions;
  193. final DateChangedCallback onChanged;
  194. final DateChangedCallback onConfirm;
  195. final DateCancelledCallback onCancel;
  196. final DatePickerTheme theme;
  197. final LocaleType locale;
  198. final BasePickerModel pickerModel;
  199. @override
  200. Duration get transitionDuration => const Duration(milliseconds: 200);
  201. @override
  202. bool get barrierDismissible => true;
  203. @override
  204. final String barrierLabel;
  205. @override
  206. Color get barrierColor => Colors.black54;
  207. AnimationController _animationController;
  208. @override
  209. AnimationController createAnimationController() {
  210. assert(_animationController == null);
  211. _animationController =
  212. BottomSheet.createAnimationController(navigator.overlay);
  213. return _animationController;
  214. }
  215. @override
  216. Widget buildPage(BuildContext context, Animation<double> animation,
  217. Animation<double> secondaryAnimation) {
  218. Widget bottomSheet = MediaQuery.removePadding(
  219. context: context,
  220. removeTop: true,
  221. child: _DatePickerComponent(
  222. onChanged: onChanged,
  223. locale: this.locale,
  224. route: this,
  225. pickerModel: pickerModel,
  226. ),
  227. );
  228. ThemeData inheritTheme = Theme.of(context, shadowThemeOnly: true);
  229. if (inheritTheme != null) {
  230. bottomSheet = new Theme(data: inheritTheme, child: bottomSheet);
  231. }
  232. return bottomSheet;
  233. }
  234. }
  235. class _DatePickerComponent extends StatefulWidget {
  236. _DatePickerComponent({
  237. Key key,
  238. @required this.route,
  239. this.onChanged,
  240. this.locale,
  241. this.pickerModel,
  242. }) : super(key: key);
  243. final DateChangedCallback onChanged;
  244. final _DatePickerRoute route;
  245. final LocaleType locale;
  246. final BasePickerModel pickerModel;
  247. @override
  248. State<StatefulWidget> createState() {
  249. return _DatePickerState();
  250. }
  251. }
  252. class _DatePickerState extends State<_DatePickerComponent> {
  253. FixedExtentScrollController leftScrollCtrl, middleScrollCtrl, rightScrollCtrl;
  254. @override
  255. void initState() {
  256. super.initState();
  257. refreshScrollOffset();
  258. }
  259. void refreshScrollOffset() {
  260. // print('refreshScrollOffset ${widget.pickerModel.currentRightIndex()}');
  261. leftScrollCtrl = FixedExtentScrollController(
  262. initialItem: widget.pickerModel.currentLeftIndex());
  263. middleScrollCtrl = FixedExtentScrollController(
  264. initialItem: widget.pickerModel.currentMiddleIndex());
  265. rightScrollCtrl = FixedExtentScrollController(
  266. initialItem: widget.pickerModel.currentRightIndex());
  267. }
  268. @override
  269. Widget build(BuildContext context) {
  270. DatePickerTheme theme = widget.route.theme;
  271. return GestureDetector(
  272. child: AnimatedBuilder(
  273. animation: widget.route.animation,
  274. builder: (BuildContext context, Widget child) {
  275. final double bottomPadding = MediaQuery.of(context).padding.bottom;
  276. return ClipRect(
  277. child: CustomSingleChildLayout(
  278. delegate: _BottomPickerLayout(
  279. widget.route.animation.value,
  280. theme,
  281. showTitleActions: widget.route.showTitleActions,
  282. bottomPadding: bottomPadding,
  283. ),
  284. child: GestureDetector(
  285. child: Material(
  286. color: theme.backgroundColor ?? Colors.white,
  287. child: _renderPickerView(theme),
  288. ),
  289. ),
  290. ),
  291. );
  292. },
  293. ),
  294. );
  295. }
  296. void _notifyDateChanged() {
  297. if (widget.onChanged != null) {
  298. widget.onChanged(widget.pickerModel.finalTime());
  299. }
  300. }
  301. Widget _renderPickerView(DatePickerTheme theme) {
  302. Widget itemView = _renderItemView(theme);
  303. if (widget.route.showTitleActions) {
  304. return Column(
  305. children: <Widget>[
  306. _renderTitleActionsView(theme),
  307. itemView,
  308. ],
  309. );
  310. }
  311. return itemView;
  312. }
  313. Widget _renderColumnView(
  314. ValueKey key,
  315. DatePickerTheme theme,
  316. StringAtIndexCallBack stringAtIndexCB,
  317. ScrollController scrollController,
  318. int layoutProportion,
  319. ValueChanged<int> selectedChangedWhenScrolling,
  320. ValueChanged<int> selectedChangedWhenScrollEnd,
  321. ) {
  322. return Expanded(
  323. flex: layoutProportion,
  324. child: Container(
  325. padding: EdgeInsets.all(8.0),
  326. height: theme.containerHeight,
  327. decoration: BoxDecoration(color: theme.backgroundColor ?? Colors.white),
  328. child: NotificationListener(
  329. onNotification: (ScrollNotification notification) {
  330. if (notification.depth == 0 &&
  331. selectedChangedWhenScrollEnd != null &&
  332. notification is ScrollEndNotification &&
  333. notification.metrics is FixedExtentMetrics) {
  334. final FixedExtentMetrics metrics = notification.metrics;
  335. final int currentItemIndex = metrics.itemIndex;
  336. selectedChangedWhenScrollEnd(currentItemIndex);
  337. }
  338. return false;
  339. },
  340. child: CupertinoPicker.builder(
  341. key: key,
  342. backgroundColor: theme.backgroundColor ?? Colors.white,
  343. scrollController: scrollController,
  344. itemExtent: theme.itemHeight,
  345. onSelectedItemChanged: (int index) {
  346. selectedChangedWhenScrolling(index);
  347. },
  348. useMagnifier: true,
  349. itemBuilder: (BuildContext context, int index) {
  350. final content = stringAtIndexCB(index);
  351. if (content == null) {
  352. return null;
  353. }
  354. return Container(
  355. height: theme.itemHeight,
  356. alignment: Alignment.center,
  357. child: Text(
  358. content,
  359. style: theme.itemStyle,
  360. textAlign: TextAlign.start,
  361. ),
  362. );
  363. },
  364. ),
  365. ),
  366. ),
  367. );
  368. }
  369. Widget _renderItemView(DatePickerTheme theme) {
  370. return Container(
  371. color: theme.backgroundColor ?? Colors.white,
  372. child: Row(
  373. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  374. children: <Widget>[
  375. Container(
  376. child: widget.pickerModel.layoutProportions()[0] > 0
  377. ? _renderColumnView(
  378. ValueKey(widget.pickerModel.currentLeftIndex()),
  379. theme,
  380. widget.pickerModel.leftStringAtIndex,
  381. leftScrollCtrl,
  382. widget.pickerModel.layoutProportions()[0], (index) {
  383. widget.pickerModel.setLeftIndex(index);
  384. }, (index) {
  385. setState(() {
  386. refreshScrollOffset();
  387. _notifyDateChanged();
  388. });
  389. })
  390. : null,
  391. ),
  392. Text(
  393. widget.pickerModel.leftDivider(),
  394. style: theme.itemStyle,
  395. ),
  396. Container(
  397. child: widget.pickerModel.layoutProportions()[1] > 0
  398. ? _renderColumnView(
  399. ValueKey(widget.pickerModel.currentLeftIndex()),
  400. theme,
  401. widget.pickerModel.middleStringAtIndex,
  402. middleScrollCtrl,
  403. widget.pickerModel.layoutProportions()[1], (index) {
  404. widget.pickerModel.setMiddleIndex(index);
  405. }, (index) {
  406. setState(() {
  407. refreshScrollOffset();
  408. _notifyDateChanged();
  409. });
  410. })
  411. : null,
  412. ),
  413. Text(
  414. widget.pickerModel.rightDivider(),
  415. style: theme.itemStyle,
  416. ),
  417. Container(
  418. child: widget.pickerModel.layoutProportions()[2] > 0
  419. ? _renderColumnView(
  420. ValueKey(widget.pickerModel.currentMiddleIndex() * 100 +
  421. widget.pickerModel.currentLeftIndex()),
  422. theme,
  423. widget.pickerModel.rightStringAtIndex,
  424. rightScrollCtrl,
  425. widget.pickerModel.layoutProportions()[2], (index) {
  426. widget.pickerModel.setRightIndex(index);
  427. _notifyDateChanged();
  428. }, null)
  429. : null,
  430. ),
  431. ],
  432. ),
  433. );
  434. }
  435. // Title View
  436. Widget _renderTitleActionsView(DatePickerTheme theme) {
  437. final done = _localeDone();
  438. final cancel = _localeCancel();
  439. return Container(
  440. height: theme.titleHeight,
  441. decoration: BoxDecoration(
  442. color: theme.headerColor ?? theme.backgroundColor ?? Colors.white,
  443. ),
  444. child: Row(
  445. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  446. children: <Widget>[
  447. Container(
  448. height: theme.titleHeight,
  449. child: CupertinoButton(
  450. pressedOpacity: 0.3,
  451. padding: EdgeInsets.only(left: 16, top: 0),
  452. child: Text(
  453. '$cancel',
  454. style: theme.cancelStyle,
  455. ),
  456. onPressed: () {
  457. Navigator.pop(context);
  458. if (widget.route.onCancel != null) {
  459. widget.route.onCancel();
  460. }
  461. },
  462. ),
  463. ),
  464. Container(
  465. height: theme.titleHeight,
  466. child: CupertinoButton(
  467. pressedOpacity: 0.3,
  468. padding: EdgeInsets.only(right: 16, top: 0),
  469. child: Text(
  470. '$done',
  471. style: theme.doneStyle,
  472. ),
  473. onPressed: () {
  474. Navigator.pop(context, widget.pickerModel.finalTime());
  475. if (widget.route.onConfirm != null) {
  476. widget.route.onConfirm(widget.pickerModel.finalTime());
  477. }
  478. },
  479. ),
  480. ),
  481. ],
  482. ),
  483. );
  484. }
  485. String _localeDone() {
  486. return i18nObjInLocale(widget.locale)['done'];
  487. }
  488. String _localeCancel() {
  489. return i18nObjInLocale(widget.locale)['cancel'];
  490. }
  491. }
  492. class _BottomPickerLayout extends SingleChildLayoutDelegate {
  493. _BottomPickerLayout(
  494. this.progress,
  495. this.theme, {
  496. this.itemCount,
  497. this.showTitleActions,
  498. this.bottomPadding = 0,
  499. });
  500. final double progress;
  501. final int itemCount;
  502. final bool showTitleActions;
  503. final DatePickerTheme theme;
  504. final double bottomPadding;
  505. @override
  506. BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
  507. double maxHeight = theme.containerHeight;
  508. if (showTitleActions) {
  509. maxHeight += theme.titleHeight;
  510. }
  511. return BoxConstraints(
  512. minWidth: constraints.maxWidth,
  513. maxWidth: constraints.maxWidth,
  514. minHeight: 0.0,
  515. maxHeight: maxHeight + bottomPadding,
  516. );
  517. }
  518. @override
  519. Offset getPositionForChild(Size size, Size childSize) {
  520. final height = size.height - childSize.height * progress;
  521. return Offset(0.0, height);
  522. }
  523. @override
  524. bool shouldRelayout(_BottomPickerLayout oldDelegate) {
  525. return progress != oldDelegate.progress;
  526. }
  527. }