flutter_datetime_picker.dart 13 KB

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