flutter_datetime_picker.dart 13 KB

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