flutter_datetime_picker.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  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. DatePickerTheme? theme,
  185. this.barrierLabel,
  186. this.locale,
  187. RouteSettings? settings,
  188. BasePickerModel? 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 LocaleType? locale;
  197. final DatePickerTheme theme;
  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. required this.pickerModel,
  236. this.onChanged,
  237. this.locale,
  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. late FixedExtentScrollController leftScrollCtrl,
  250. middleScrollCtrl,
  251. rightScrollCtrl;
  252. @override
  253. void initState() {
  254. super.initState();
  255. refreshScrollOffset();
  256. }
  257. void refreshScrollOffset() {
  258. // print('refreshScrollOffset ${widget.pickerModel.currentRightIndex()}');
  259. leftScrollCtrl = FixedExtentScrollController(
  260. initialItem: widget.pickerModel.currentLeftIndex());
  261. middleScrollCtrl = FixedExtentScrollController(
  262. initialItem: widget.pickerModel.currentMiddleIndex());
  263. rightScrollCtrl = FixedExtentScrollController(
  264. initialItem: widget.pickerModel.currentRightIndex());
  265. }
  266. @override
  267. Widget build(BuildContext context) {
  268. DatePickerTheme theme = widget.route.theme;
  269. return GestureDetector(
  270. child: AnimatedBuilder(
  271. animation: widget.route.animation!,
  272. builder: (BuildContext context, Widget? child) {
  273. final double bottomPadding = MediaQuery.of(context).padding.bottom;
  274. return ClipRect(
  275. child: CustomSingleChildLayout(
  276. delegate: _BottomPickerLayout(
  277. widget.route.animation!.value,
  278. theme,
  279. showTitleActions: widget.route.showTitleActions!,
  280. bottomPadding: bottomPadding,
  281. ),
  282. child: GestureDetector(
  283. child: Material(
  284. color: theme.backgroundColor,
  285. child: _renderPickerView(theme),
  286. ),
  287. ),
  288. ),
  289. );
  290. },
  291. ),
  292. );
  293. }
  294. void _notifyDateChanged() {
  295. if (widget.onChanged != null) {
  296. widget.onChanged!(widget.pickerModel.finalTime()!);
  297. }
  298. }
  299. Widget _renderPickerView(DatePickerTheme theme) {
  300. Widget itemView = _renderItemView(theme);
  301. if (widget.route.showTitleActions == true) {
  302. return Column(
  303. children: <Widget>[
  304. _renderTitleActionsView(theme),
  305. itemView,
  306. ],
  307. );
  308. }
  309. return itemView;
  310. }
  311. Widget _renderColumnView(
  312. ValueKey key,
  313. DatePickerTheme theme,
  314. StringAtIndexCallBack stringAtIndexCB,
  315. ScrollController scrollController,
  316. int layoutProportion,
  317. ValueChanged<int> selectedChangedWhenScrolling,
  318. ValueChanged<int> selectedChangedWhenScrollEnd,
  319. ) {
  320. return Expanded(
  321. flex: layoutProportion,
  322. child: Container(
  323. padding: EdgeInsets.all(8.0),
  324. height: theme.containerHeight,
  325. decoration: BoxDecoration(color: theme.backgroundColor),
  326. child: NotificationListener(
  327. onNotification: (ScrollNotification notification) {
  328. if (notification.depth == 0 &&
  329. notification is ScrollEndNotification &&
  330. notification.metrics is FixedExtentMetrics) {
  331. final FixedExtentMetrics metrics =
  332. notification.metrics as FixedExtentMetrics;
  333. final int currentItemIndex = metrics.itemIndex;
  334. selectedChangedWhenScrollEnd(currentItemIndex);
  335. }
  336. return false;
  337. },
  338. child: CupertinoPicker.builder(
  339. key: key,
  340. backgroundColor: theme.backgroundColor,
  341. scrollController: scrollController as FixedExtentScrollController,
  342. itemExtent: theme.itemHeight,
  343. onSelectedItemChanged: (int index) {
  344. selectedChangedWhenScrolling(index);
  345. },
  346. useMagnifier: true,
  347. itemBuilder: (BuildContext context, int index) {
  348. final content = stringAtIndexCB(index);
  349. if (content == null) {
  350. return null;
  351. }
  352. return Container(
  353. height: theme.itemHeight,
  354. alignment: Alignment.center,
  355. child: Text(
  356. content,
  357. style: theme.itemStyle,
  358. textAlign: TextAlign.start,
  359. ),
  360. );
  361. },
  362. ),
  363. ),
  364. ),
  365. );
  366. }
  367. Widget _renderItemView(DatePickerTheme theme) {
  368. return Container(
  369. color: theme.backgroundColor,
  370. child: Row(
  371. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  372. children: <Widget>[
  373. Container(
  374. child: widget.pickerModel.layoutProportions()[0] > 0
  375. ? _renderColumnView(
  376. ValueKey(widget.pickerModel.currentLeftIndex()),
  377. theme,
  378. widget.pickerModel.leftStringAtIndex,
  379. leftScrollCtrl,
  380. widget.pickerModel.layoutProportions()[0], (index) {
  381. widget.pickerModel.setLeftIndex(index);
  382. }, (index) {
  383. setState(() {
  384. refreshScrollOffset();
  385. _notifyDateChanged();
  386. });
  387. })
  388. : null,
  389. ),
  390. Text(
  391. widget.pickerModel.leftDivider(),
  392. style: theme.itemStyle,
  393. ),
  394. Container(
  395. child: widget.pickerModel.layoutProportions()[1] > 0
  396. ? _renderColumnView(
  397. ValueKey(widget.pickerModel.currentLeftIndex()),
  398. theme,
  399. widget.pickerModel.middleStringAtIndex,
  400. middleScrollCtrl,
  401. widget.pickerModel.layoutProportions()[1], (index) {
  402. widget.pickerModel.setMiddleIndex(index);
  403. }, (index) {
  404. setState(() {
  405. refreshScrollOffset();
  406. _notifyDateChanged();
  407. });
  408. })
  409. : null,
  410. ),
  411. Text(
  412. widget.pickerModel.rightDivider(),
  413. style: theme.itemStyle,
  414. ),
  415. Container(
  416. child: widget.pickerModel.layoutProportions()[2] > 0
  417. ? _renderColumnView(
  418. ValueKey(widget.pickerModel.currentMiddleIndex() * 100 +
  419. widget.pickerModel.currentLeftIndex()),
  420. theme,
  421. widget.pickerModel.rightStringAtIndex,
  422. rightScrollCtrl,
  423. widget.pickerModel.layoutProportions()[2], (index) {
  424. widget.pickerModel.setRightIndex(index);
  425. }, (index) {
  426. setState(() {
  427. refreshScrollOffset();
  428. _notifyDateChanged();
  429. });
  430. })
  431. : null,
  432. ),
  433. ],
  434. ),
  435. );
  436. }
  437. // Title View
  438. Widget _renderTitleActionsView(DatePickerTheme theme) {
  439. final done = _localeDone();
  440. final cancel = _localeCancel();
  441. return Container(
  442. height: theme.titleHeight,
  443. decoration: BoxDecoration(
  444. color: theme.headerColor ?? theme.backgroundColor,
  445. ),
  446. child: Row(
  447. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  448. children: <Widget>[
  449. Container(
  450. height: theme.titleHeight,
  451. child: CupertinoButton(
  452. pressedOpacity: 0.3,
  453. padding: EdgeInsets.only(left: 16, top: 0),
  454. child: Text(
  455. '$cancel',
  456. style: theme.cancelStyle,
  457. ),
  458. onPressed: () {
  459. Navigator.pop(context);
  460. if (widget.route.onCancel != null) {
  461. widget.route.onCancel!();
  462. }
  463. },
  464. ),
  465. ),
  466. Container(
  467. height: theme.titleHeight,
  468. child: CupertinoButton(
  469. pressedOpacity: 0.3,
  470. padding: EdgeInsets.only(right: 16, top: 0),
  471. child: Text(
  472. '$done',
  473. style: theme.doneStyle,
  474. ),
  475. onPressed: () {
  476. Navigator.pop(context, widget.pickerModel.finalTime());
  477. if (widget.route.onConfirm != null) {
  478. widget.route.onConfirm!(widget.pickerModel.finalTime()!);
  479. }
  480. },
  481. ),
  482. ),
  483. ],
  484. ),
  485. );
  486. }
  487. String _localeDone() {
  488. return i18nObjInLocale(widget.locale)['done'] as String;
  489. }
  490. String _localeCancel() {
  491. return i18nObjInLocale(widget.locale)['cancel'] as String;
  492. }
  493. }
  494. class _BottomPickerLayout extends SingleChildLayoutDelegate {
  495. _BottomPickerLayout(
  496. this.progress,
  497. this.theme, {
  498. this.itemCount,
  499. this.showTitleActions,
  500. this.bottomPadding = 0,
  501. });
  502. final double progress;
  503. final int? itemCount;
  504. final bool? showTitleActions;
  505. final DatePickerTheme theme;
  506. final double bottomPadding;
  507. @override
  508. BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
  509. double maxHeight = theme.containerHeight;
  510. if (showTitleActions == true) {
  511. maxHeight += theme.titleHeight;
  512. }
  513. return BoxConstraints(
  514. minWidth: constraints.maxWidth,
  515. maxWidth: constraints.maxWidth,
  516. minHeight: 0.0,
  517. maxHeight: maxHeight + bottomPadding,
  518. );
  519. }
  520. @override
  521. Offset getPositionForChild(Size size, Size childSize) {
  522. final height = size.height - childSize.height * progress;
  523. return Offset(0.0, height);
  524. }
  525. @override
  526. bool shouldRelayout(_BottomPickerLayout oldDelegate) {
  527. return progress != oldDelegate.progress;
  528. }
  529. }