flutter_datetime_picker.dart 17 KB

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