flutter_datetime_picker.dart 16 KB

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