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. ThemeData inheritTheme = Theme.of(context, shadowThemeOnly: true);
  199. if (inheritTheme != null) {
  200. bottomSheet = new Theme(data: inheritTheme, child: bottomSheet);
  201. }
  202. return bottomSheet;
  203. }
  204. }
  205. class _DatePickerComponent extends StatefulWidget {
  206. _DatePickerComponent(
  207. {Key key, @required this.route, this.onChanged, this.locale, this.pickerModel});
  208. final DateChangedCallback onChanged;
  209. final _DatePickerRoute route;
  210. final LocaleType locale;
  211. final BasePickerModel pickerModel;
  212. @override
  213. State<StatefulWidget> createState() {
  214. return _DatePickerState();
  215. }
  216. }
  217. class _DatePickerState extends State<_DatePickerComponent> {
  218. FixedExtentScrollController leftScrollCtrl, middleScrollCtrl, rightScrollCtrl;
  219. @override
  220. void initState() {
  221. super.initState();
  222. refreshScrollOffset();
  223. }
  224. void refreshScrollOffset() {
  225. // print('refreshScrollOffset ${widget.pickerModel.currentRightIndex()}');
  226. leftScrollCtrl =
  227. new FixedExtentScrollController(initialItem: widget.pickerModel.currentLeftIndex());
  228. middleScrollCtrl =
  229. new FixedExtentScrollController(initialItem: widget.pickerModel.currentMiddleIndex());
  230. rightScrollCtrl =
  231. new FixedExtentScrollController(initialItem: widget.pickerModel.currentRightIndex());
  232. }
  233. @override
  234. Widget build(BuildContext context) {
  235. DatePickerTheme theme = widget.route.theme;
  236. return GestureDetector(
  237. child: AnimatedBuilder(
  238. animation: widget.route.animation,
  239. builder: (BuildContext context, Widget child) {
  240. final double bottomPadding = MediaQuery.of(context).padding.bottom;
  241. return ClipRect(
  242. child: CustomSingleChildLayout(
  243. delegate: _BottomPickerLayout(widget.route.animation.value, theme,
  244. showTitleActions: widget.route.showTitleActions, bottomPadding: bottomPadding),
  245. child: GestureDetector(
  246. child: Material(
  247. color: theme.backgroundColor ?? Colors.white,
  248. child: _renderPickerView(theme),
  249. ),
  250. ),
  251. ),
  252. );
  253. },
  254. ),
  255. );
  256. }
  257. void _notifyDateChanged() {
  258. if (widget.onChanged != null) {
  259. widget.onChanged(widget.pickerModel.finalTime());
  260. }
  261. }
  262. Widget _renderPickerView(DatePickerTheme theme) {
  263. Widget itemView = _renderItemView(theme);
  264. if (widget.route.showTitleActions) {
  265. return Column(
  266. children: <Widget>[
  267. _renderTitleActionsView(theme),
  268. itemView,
  269. ],
  270. );
  271. }
  272. return itemView;
  273. }
  274. Widget _renderColumnView(
  275. ValueKey key,
  276. DatePickerTheme theme,
  277. StringAtIndexCallBack stringAtIndexCB,
  278. ScrollController scrollController,
  279. int layoutProportion,
  280. ValueChanged<int> selectedChangedWhenScrolling,
  281. ValueChanged<int> selectedChangedWhenScrollEnd) {
  282. return Expanded(
  283. flex: layoutProportion,
  284. child: Container(
  285. padding: EdgeInsets.all(8.0),
  286. height: theme.containerHeight,
  287. decoration: BoxDecoration(color: theme.backgroundColor ?? Colors.white),
  288. child: NotificationListener(
  289. onNotification: (ScrollNotification notification) {
  290. if (notification.depth == 0 &&
  291. selectedChangedWhenScrollEnd != null &&
  292. notification is ScrollEndNotification &&
  293. notification.metrics is FixedExtentMetrics) {
  294. final FixedExtentMetrics metrics = notification.metrics;
  295. final int currentItemIndex = metrics.itemIndex;
  296. selectedChangedWhenScrollEnd(currentItemIndex);
  297. }
  298. return false;
  299. },
  300. child: CupertinoPicker.builder(
  301. key: key,
  302. backgroundColor: theme.backgroundColor ?? Colors.white,
  303. scrollController: scrollController,
  304. itemExtent: theme.itemHeight,
  305. onSelectedItemChanged: (int index) {
  306. selectedChangedWhenScrolling(index);
  307. },
  308. useMagnifier: true,
  309. itemBuilder: (BuildContext context, int index) {
  310. final content = stringAtIndexCB(index);
  311. if (content == null) {
  312. return null;
  313. }
  314. return Container(
  315. height: theme.itemHeight,
  316. alignment: Alignment.center,
  317. child: Text(
  318. content,
  319. style: theme.itemStyle,
  320. textAlign: TextAlign.start,
  321. ),
  322. );
  323. }))),
  324. );
  325. }
  326. Widget _renderItemView(DatePickerTheme theme) {
  327. return Container(
  328. color: theme.backgroundColor ?? Colors.white,
  329. child: Row(
  330. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  331. children: <Widget>[
  332. Container(
  333. child: widget.pickerModel.layoutProportions()[0] > 0
  334. ? _renderColumnView(
  335. ValueKey(widget.pickerModel.currentLeftIndex()),
  336. theme,
  337. widget.pickerModel.leftStringAtIndex,
  338. leftScrollCtrl,
  339. widget.pickerModel.layoutProportions()[0], (index) {
  340. widget.pickerModel.setLeftIndex(index);
  341. }, (index) {
  342. setState(() {
  343. refreshScrollOffset();
  344. _notifyDateChanged();
  345. });
  346. })
  347. : null,
  348. ),
  349. Text(
  350. widget.pickerModel.leftDivider(),
  351. style: theme.itemStyle,
  352. ),
  353. Container(
  354. child: widget.pickerModel.layoutProportions()[1] > 0
  355. ? _renderColumnView(
  356. ValueKey(widget.pickerModel.currentLeftIndex()),
  357. theme,
  358. widget.pickerModel.middleStringAtIndex,
  359. middleScrollCtrl,
  360. widget.pickerModel.layoutProportions()[1], (index) {
  361. widget.pickerModel.setMiddleIndex(index);
  362. }, (index) {
  363. setState(() {
  364. refreshScrollOffset();
  365. _notifyDateChanged();
  366. });
  367. })
  368. : null,
  369. ),
  370. Text(
  371. widget.pickerModel.rightDivider(),
  372. style: theme.itemStyle,
  373. ),
  374. Container(
  375. child: widget.pickerModel.layoutProportions()[2] > 0
  376. ? _renderColumnView(
  377. ValueKey(widget.pickerModel.currentMiddleIndex() * 100 +
  378. widget.pickerModel.currentLeftIndex()),
  379. theme,
  380. widget.pickerModel.rightStringAtIndex,
  381. rightScrollCtrl,
  382. widget.pickerModel.layoutProportions()[2], (index) {
  383. widget.pickerModel.setRightIndex(index);
  384. _notifyDateChanged();
  385. }, null)
  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. }