flutter_datetime_picker.dart 16 KB

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