flutter_datetime_picker.dart 13 KB

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