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