flutter_datetime_picker.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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:
  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. leftScrollCtrl = new FixedExtentScrollController(
  176. initialItem: widget.pickerModel.currentLeftIndex());
  177. middleScrollCtrl = new FixedExtentScrollController(
  178. initialItem: widget.pickerModel.currentMiddleIndex());
  179. rightScrollCtrl = new FixedExtentScrollController(
  180. initialItem: widget.pickerModel.currentRightIndex());
  181. }
  182. @override
  183. Widget build(BuildContext context) {
  184. return new GestureDetector(
  185. child: new AnimatedBuilder(
  186. animation: widget.route.animation,
  187. builder: (BuildContext context, Widget child) {
  188. return new ClipRect(
  189. child: new CustomSingleChildLayout(
  190. delegate: new _BottomPickerLayout(widget.route.animation.value,
  191. showTitleActions: widget.route.showTitleActions),
  192. child: new GestureDetector(
  193. child: Material(
  194. color: Colors.transparent,
  195. child: _renderPickerView(),
  196. ),
  197. ),
  198. ),
  199. );
  200. },
  201. ),
  202. );
  203. }
  204. void _notifyDateChanged() {
  205. if (widget.onChanged != null) {
  206. widget.onChanged(widget.pickerModel.finalTime());
  207. }
  208. }
  209. Widget _renderPickerView() {
  210. Widget itemView = _renderItemView();
  211. if (widget.route.showTitleActions) {
  212. return Column(
  213. children: <Widget>[
  214. _renderTitleActionsView(),
  215. itemView,
  216. ],
  217. );
  218. }
  219. return itemView;
  220. }
  221. Widget _renderColumnView(
  222. StringAtIndexCallBack stringAtIndexCB,
  223. ScrollController scrollController,
  224. int layoutProportion,
  225. ValueChanged<int> selectedChanged) {
  226. return Expanded(
  227. flex: layoutProportion,
  228. child: Container(
  229. padding: EdgeInsets.all(8.0),
  230. height: _kDatePickerHeight,
  231. decoration: BoxDecoration(color: Colors.white),
  232. child: CupertinoPicker.builder(
  233. backgroundColor: Colors.white,
  234. scrollController: scrollController,
  235. itemExtent: _kDatePickerItemHeight,
  236. onSelectedItemChanged: (int index) {
  237. selectedChanged(index);
  238. },
  239. useMagnifier: true,
  240. itemBuilder: (BuildContext context, int index) {
  241. final content = stringAtIndexCB(index);
  242. if (content == null) {
  243. return null;
  244. }
  245. return Container(
  246. height: _kDatePickerItemHeight,
  247. alignment: Alignment.center,
  248. child: Text(
  249. content,
  250. style: TextStyle(
  251. color: Color(0xFF000046),
  252. fontSize: _kDatePickerFontSize),
  253. textAlign: TextAlign.start,
  254. ),
  255. );
  256. })),
  257. );
  258. }
  259. Widget _renderItemView() {
  260. return Container(
  261. color: Colors.white,
  262. child: Row(
  263. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  264. children: <Widget>[
  265. _renderColumnView(
  266. widget.pickerModel.leftStringAtIndex,
  267. leftScrollCtrl,
  268. widget.pickerModel.layoutProportions()[0], (index) {
  269. widget.pickerModel.setLeftIndex(index);
  270. _notifyDateChanged();
  271. }),
  272. Text(
  273. widget.pickerModel.leftDivider(),
  274. style: TextStyle(
  275. color: Color(0xFF000046), fontSize: _kDatePickerFontSize),
  276. ),
  277. _renderColumnView(
  278. widget.pickerModel.middleStringAtIndex,
  279. middleScrollCtrl,
  280. widget.pickerModel.layoutProportions()[1], (index) {
  281. widget.pickerModel.setMiddleIndex(index);
  282. _notifyDateChanged();
  283. }),
  284. Text(
  285. widget.pickerModel.rightDivider(),
  286. style: TextStyle(
  287. color: Color(0xFF000046), fontSize: _kDatePickerFontSize),
  288. ),
  289. _renderColumnView(
  290. widget.pickerModel.rightStringAtIndex,
  291. rightScrollCtrl,
  292. widget.pickerModel.layoutProportions()[2], (index) {
  293. widget.pickerModel.setRightIndex(index);
  294. _notifyDateChanged();
  295. }),
  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. if (widget.locale.matchAsPrefix('cn') == null) {
  347. return 'Done';
  348. } else {
  349. return '确定';
  350. }
  351. }
  352. String _localeCancel() {
  353. if (widget.locale.matchAsPrefix('cn') == null) {
  354. return 'Cancel';
  355. } else {
  356. return '取消';
  357. }
  358. }
  359. }
  360. class _BottomPickerLayout extends SingleChildLayoutDelegate {
  361. _BottomPickerLayout(this.progress, {this.itemCount, this.showTitleActions});
  362. final double progress;
  363. final int itemCount;
  364. final bool showTitleActions;
  365. @override
  366. BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
  367. double maxHeight = _kDatePickerHeight;
  368. if (showTitleActions) {
  369. maxHeight += _kDatePickerTitleHeight;
  370. }
  371. return new BoxConstraints(
  372. minWidth: constraints.maxWidth,
  373. maxWidth: constraints.maxWidth,
  374. minHeight: 0.0,
  375. maxHeight: maxHeight);
  376. }
  377. @override
  378. Offset getPositionForChild(Size size, Size childSize) {
  379. double height = size.height - childSize.height * progress;
  380. return new Offset(0.0, height);
  381. }
  382. @override
  383. bool shouldRelayout(_BottomPickerLayout oldDelegate) {
  384. return progress != oldDelegate.progress;
  385. }
  386. }