flutter_datetime_picker.dart 15 KB

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