flutter_datetime_picker.dart 15 KB

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