flutter_datetime_picker.dart 15 KB

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