flutter_datetime_picker.dart 15 KB

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