From 431a2b19d227be867bdc8d34ba34426704038eea Mon Sep 17 00:00:00 2001 From: appvinio-filip-gawel Date: Mon, 16 Aug 2021 23:39:47 +0200 Subject: [PATCH 1/3] picker as widget --- example/lib/main.dart | 30 +++ lib/flutter_datetime_picker.dart | 432 ++++++++++++++++++------------- 2 files changed, 289 insertions(+), 173 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 1ab0e793..5fa37cb0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -250,9 +250,39 @@ class HomePage extends StatelessWidget { 'show custom time picker,\nyou can custom picker model like this', style: TextStyle(color: Colors.blue), )), + TextButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => DatePickerWidgetScreen())); + }, + child: Text( + 'show date picker widget', + style: TextStyle(color: Colors.blue), + )), ], ), ), ); } } + +class DatePickerWidgetScreen extends StatefulWidget { + const DatePickerWidgetScreen({Key? key}) : super(key: key); + + @override + _DatePickerWidgetScreenState createState() => _DatePickerWidgetScreenState(); +} + +class _DatePickerWidgetScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Date picker widget'), + ), + body: Center( + child: DatePickerWidget(), + ), + ); + } +} diff --git a/lib/flutter_datetime_picker.dart b/lib/flutter_datetime_picker.dart index dba6b566..4c41241e 100644 --- a/lib/flutter_datetime_picker.dart +++ b/lib/flutter_datetime_picker.dart @@ -34,7 +34,7 @@ class DatePicker { }) async { return await Navigator.push( context, - _DatePickerRoute( + DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -69,7 +69,7 @@ class DatePicker { }) async { return await Navigator.push( context, - _DatePickerRoute( + DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -102,7 +102,7 @@ class DatePicker { }) async { return await Navigator.push( context, - _DatePickerRoute( + DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -136,7 +136,7 @@ class DatePicker { }) async { return await Navigator.push( context, - _DatePickerRoute( + DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -170,7 +170,7 @@ class DatePicker { }) async { return await Navigator.push( context, - _DatePickerRoute( + DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -185,8 +185,8 @@ class DatePicker { } } -class _DatePickerRoute extends PopupRoute { - _DatePickerRoute({ +class DatePickerRoute extends PopupRoute { + DatePickerRoute({ this.showTitleActions, this.onChanged, this.onConfirm, @@ -236,7 +236,7 @@ class _DatePickerRoute extends PopupRoute { Widget bottomSheet = MediaQuery.removePadding( context: context, removeTop: true, - child: _DatePickerComponent( + child: DatePickerComponent( onChanged: onChanged, locale: this.locale, route: this, @@ -247,8 +247,54 @@ class _DatePickerRoute extends PopupRoute { } } -class _DatePickerComponent extends StatefulWidget { - _DatePickerComponent({ +class DatePickerWidget extends StatelessWidget { + DatePickerWidget({ + Key? key, + this.showTitleActions = true, + this.onChanged, + this.onConfirm, + this.onCancel, + this.locale = LocaleType.en, + this.pickerModel, + this.theme, + }) : route = DatePickerRoute( + showTitleActions: showTitleActions, + onChanged: onChanged, + onConfirm: onConfirm, + onCancel: onCancel, + locale: locale, + theme: theme, + pickerModel: pickerModel, + ), + super(key: key); + + final bool showTitleActions; + final DateChangedCallback? onChanged; + final DateChangedCallback? onConfirm; + final DateCancelledCallback? onCancel; + final LocaleType locale; + final BasePickerModel? pickerModel; + final DatePickerTheme? theme; + final DatePickerRoute route; + + @override + Widget build(BuildContext context) { + return _RenderPickerView( + route: route, + itemView: _ItemView( + pickerModel: route.pickerModel, + onChanged: route.onChanged, + theme: route.theme, + ), + titleActionsView: _TitleActionsView( + route: route, + ), + ); + } +} + +class DatePickerComponent extends StatelessWidget { + DatePickerComponent({ Key? key, required this.route, required this.pickerModel, @@ -258,59 +304,37 @@ class _DatePickerComponent extends StatefulWidget { final DateChangedCallback? onChanged; - final _DatePickerRoute route; + final DatePickerRoute route; final LocaleType? locale; final BasePickerModel pickerModel; - @override - State createState() { - return _DatePickerState(); - } -} - -class _DatePickerState extends State<_DatePickerComponent> { - late FixedExtentScrollController leftScrollCtrl, - middleScrollCtrl, - rightScrollCtrl; - - @override - void initState() { - super.initState(); - refreshScrollOffset(); - } - - void refreshScrollOffset() { -// print('refreshScrollOffset ${widget.pickerModel.currentRightIndex()}'); - leftScrollCtrl = FixedExtentScrollController( - initialItem: widget.pickerModel.currentLeftIndex()); - middleScrollCtrl = FixedExtentScrollController( - initialItem: widget.pickerModel.currentMiddleIndex()); - rightScrollCtrl = FixedExtentScrollController( - initialItem: widget.pickerModel.currentRightIndex()); - } - @override Widget build(BuildContext context) { - DatePickerTheme theme = widget.route.theme; + DatePickerTheme theme = route.theme; return GestureDetector( child: AnimatedBuilder( - animation: widget.route.animation!, + animation: route.animation!, builder: (BuildContext context, Widget? child) { final double bottomPadding = MediaQuery.of(context).padding.bottom; return ClipRect( child: CustomSingleChildLayout( delegate: _BottomPickerLayout( - widget.route.animation!.value, + route.animation!.value, theme, - showTitleActions: widget.route.showTitleActions!, + showTitleActions: route.showTitleActions!, bottomPadding: bottomPadding, ), - child: GestureDetector( - child: Material( - color: theme.backgroundColor, - child: _renderPickerView(theme), + child: _RenderPickerView( + route: route, + itemView: _ItemView( + theme: theme, + onChanged: onChanged, + pickerModel: pickerModel, + ), + titleActionsView: _TitleActionsView( + route: route, ), ), ), @@ -319,86 +343,67 @@ class _DatePickerState extends State<_DatePickerComponent> { ), ); } +} - void _notifyDateChanged() { - if (widget.onChanged != null) { - widget.onChanged!(widget.pickerModel.finalTime()!); - } - } +class _RenderPickerView extends StatelessWidget { + const _RenderPickerView({ + Key? key, + required this.route, + required this.itemView, + required this.titleActionsView, + }) : super(key: key); - Widget _renderPickerView(DatePickerTheme theme) { - Widget itemView = _renderItemView(theme); - if (widget.route.showTitleActions == true) { - return Column( - children: [ - _renderTitleActionsView(theme), - itemView, - ], - ); - } - return itemView; - } + final DatePickerRoute route; - Widget _renderColumnView( - ValueKey key, - DatePickerTheme theme, - StringAtIndexCallBack stringAtIndexCB, - ScrollController scrollController, - int layoutProportion, - ValueChanged selectedChangedWhenScrolling, - ValueChanged selectedChangedWhenScrollEnd, - ) { - return Expanded( - flex: layoutProportion, - child: Container( - padding: EdgeInsets.all(8.0), - height: theme.containerHeight, - decoration: BoxDecoration(color: theme.backgroundColor), - child: NotificationListener( - onNotification: (ScrollNotification notification) { - if (notification.depth == 0 && - notification is ScrollEndNotification && - notification.metrics is FixedExtentMetrics) { - final FixedExtentMetrics metrics = - notification.metrics as FixedExtentMetrics; - final int currentItemIndex = metrics.itemIndex; - selectedChangedWhenScrollEnd(currentItemIndex); - } - return false; - }, - child: CupertinoPicker.builder( - key: key, - backgroundColor: theme.backgroundColor, - scrollController: scrollController as FixedExtentScrollController, - itemExtent: theme.itemHeight, - onSelectedItemChanged: (int index) { - selectedChangedWhenScrolling(index); - }, - useMagnifier: true, - itemBuilder: (BuildContext context, int index) { - final content = stringAtIndexCB(index); - if (content == null) { - return null; - } - return Container( - height: theme.itemHeight, - alignment: Alignment.center, - child: Text( - content, - style: theme.itemStyle, - textAlign: TextAlign.start, - ), - ); - }, - ), + final _ItemView itemView; + final _TitleActionsView titleActionsView; + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: Material( + color: route.theme.backgroundColor, + child: Column( + children: [ + if (route.showTitleActions == true) titleActionsView, + itemView, + ], ), ), ); } +} + +class _ItemView extends StatefulWidget { + const _ItemView({ + Key? key, + required this.theme, + this.onChanged, + required this.pickerModel, + }) : super(key: key); + final DatePickerTheme theme; + final DateChangedCallback? onChanged; + final BasePickerModel pickerModel; + + @override + __ItemViewState createState() => __ItemViewState(); +} + +class __ItemViewState extends State<_ItemView> { + late FixedExtentScrollController leftScrollCtrl, + middleScrollCtrl, + rightScrollCtrl; + + @override + void initState() { + super.initState(); + refreshScrollOffset(); + } - Widget _renderItemView(DatePickerTheme theme) { + @override + Widget build(BuildContext context) { return Container( - color: theme.backgroundColor, + color: widget.theme.backgroundColor, child: Directionality( textDirection: TextDirection.ltr, child: Row( @@ -408,7 +413,7 @@ class _DatePickerState extends State<_DatePickerComponent> { child: widget.pickerModel.layoutProportions()[0] > 0 ? _renderColumnView( ValueKey(widget.pickerModel.currentLeftIndex()), - theme, + widget.theme, widget.pickerModel.leftStringAtIndex, leftScrollCtrl, widget.pickerModel.layoutProportions()[0], (index) { @@ -423,13 +428,13 @@ class _DatePickerState extends State<_DatePickerComponent> { ), Text( widget.pickerModel.leftDivider(), - style: theme.itemStyle, + style: widget.theme.itemStyle, ), Container( child: widget.pickerModel.layoutProportions()[1] > 0 ? _renderColumnView( ValueKey(widget.pickerModel.currentLeftIndex()), - theme, + widget.theme, widget.pickerModel.middleStringAtIndex, middleScrollCtrl, widget.pickerModel.layoutProportions()[1], (index) { @@ -444,14 +449,14 @@ class _DatePickerState extends State<_DatePickerComponent> { ), Text( widget.pickerModel.rightDivider(), - style: theme.itemStyle, + style: widget.theme.itemStyle, ), Container( child: widget.pickerModel.layoutProportions()[2] > 0 ? _renderColumnView( ValueKey(widget.pickerModel.currentMiddleIndex() * 100 + widget.pickerModel.currentLeftIndex()), - theme, + widget.theme, widget.pickerModel.rightStringAtIndex, rightScrollCtrl, widget.pickerModel.layoutProportions()[2], (index) { @@ -470,64 +475,77 @@ class _DatePickerState extends State<_DatePickerComponent> { ); } - // Title View - Widget _renderTitleActionsView(DatePickerTheme theme) { - final done = _localeDone(); - final cancel = _localeCancel(); - - return Container( - height: theme.titleHeight, - decoration: BoxDecoration( - color: theme.headerColor ?? theme.backgroundColor, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - height: theme.titleHeight, - child: CupertinoButton( - pressedOpacity: 0.3, - padding: EdgeInsetsDirectional.only(start: 16, top: 0), - child: Text( - '$cancel', - style: theme.cancelStyle, - ), - onPressed: () { - Navigator.pop(context); - if (widget.route.onCancel != null) { - widget.route.onCancel!(); - } - }, - ), - ), - Container( - height: theme.titleHeight, - child: CupertinoButton( - pressedOpacity: 0.3, - padding: EdgeInsetsDirectional.only(end: 16, top: 0), - child: Text( - '$done', - style: theme.doneStyle, - ), - onPressed: () { - Navigator.pop(context, widget.pickerModel.finalTime()); - if (widget.route.onConfirm != null) { - widget.route.onConfirm!(widget.pickerModel.finalTime()!); - } - }, - ), + Widget _renderColumnView( + ValueKey key, + DatePickerTheme theme, + StringAtIndexCallBack stringAtIndexCB, + ScrollController scrollController, + int layoutProportion, + ValueChanged selectedChangedWhenScrolling, + ValueChanged selectedChangedWhenScrollEnd, + ) { + return Expanded( + flex: layoutProportion, + child: Container( + padding: EdgeInsets.all(8.0), + height: theme.containerHeight, + decoration: BoxDecoration(color: theme.backgroundColor), + child: NotificationListener( + onNotification: (ScrollNotification notification) { + if (notification.depth == 0 && + notification is ScrollEndNotification && + notification.metrics is FixedExtentMetrics) { + final FixedExtentMetrics metrics = + notification.metrics as FixedExtentMetrics; + final int currentItemIndex = metrics.itemIndex; + selectedChangedWhenScrollEnd(currentItemIndex); + } + return false; + }, + child: CupertinoPicker.builder( + key: key, + backgroundColor: theme.backgroundColor, + scrollController: scrollController as FixedExtentScrollController, + itemExtent: theme.itemHeight, + onSelectedItemChanged: (int index) { + selectedChangedWhenScrolling(index); + }, + useMagnifier: true, + itemBuilder: (BuildContext context, int index) { + final content = stringAtIndexCB(index); + if (content == null) { + return null; + } + return Container( + height: theme.itemHeight, + alignment: Alignment.center, + child: Text( + content, + style: theme.itemStyle, + textAlign: TextAlign.start, + ), + ); + }, ), - ], + ), ), ); } - String _localeDone() { - return i18nObjInLocale(widget.locale)['done'] as String; + void _notifyDateChanged() { + if (widget.onChanged != null) { + widget.onChanged!(widget.pickerModel.finalTime()!); + } } - String _localeCancel() { - return i18nObjInLocale(widget.locale)['cancel'] as String; + void refreshScrollOffset() { +// print('refreshScrollOffset ${widget.pickerModel.currentRightIndex()}'); + leftScrollCtrl = FixedExtentScrollController( + initialItem: widget.pickerModel.currentLeftIndex()); + middleScrollCtrl = FixedExtentScrollController( + initialItem: widget.pickerModel.currentMiddleIndex()); + rightScrollCtrl = FixedExtentScrollController( + initialItem: widget.pickerModel.currentRightIndex()); } } @@ -572,3 +590,71 @@ class _BottomPickerLayout extends SingleChildLayoutDelegate { return progress != oldDelegate.progress; } } + +class _TitleActionsView extends StatelessWidget { + const _TitleActionsView({ + Key? key, + required this.route, + }) : super(key: key); + final DatePickerRoute route; + + @override + Widget build(BuildContext context) { + final done = _localeDone(); + final cancel = _localeCancel(); + + return Container( + height: route.theme.titleHeight, + decoration: BoxDecoration( + color: route.theme.headerColor ?? route.theme.backgroundColor, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: route.theme.titleHeight, + child: CupertinoButton( + pressedOpacity: 0.3, + padding: EdgeInsetsDirectional.only(start: 16, top: 0), + child: Text( + '$cancel', + style: route.theme.cancelStyle, + ), + onPressed: () { + Navigator.pop(context); + if (route.onCancel != null) { + route.onCancel!(); + } + }, + ), + ), + Container( + height: route.theme.titleHeight, + child: CupertinoButton( + pressedOpacity: 0.3, + padding: EdgeInsetsDirectional.only(end: 16, top: 0), + child: Text( + '$done', + style: route.theme.doneStyle, + ), + onPressed: () { + Navigator.pop(context, route.pickerModel.finalTime()); + if (route.onConfirm != null) { + route.onConfirm!(route.pickerModel.finalTime()!); + } + }, + ), + ), + ], + ), + ); + } + + String _localeDone() { + return i18nObjInLocale(route.locale)['done'] as String; + } + + String _localeCancel() { + return i18nObjInLocale(route.locale)['cancel'] as String; + } +} From 7651af4bc6b678c1f4b597545980d86ec55f5e81 Mon Sep 17 00:00:00 2001 From: appvinio-filip-gawel Date: Mon, 16 Aug 2021 23:45:14 +0200 Subject: [PATCH 2/3] make private: date picker route --- lib/flutter_datetime_picker.dart | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/flutter_datetime_picker.dart b/lib/flutter_datetime_picker.dart index 4c41241e..3a0c94fa 100644 --- a/lib/flutter_datetime_picker.dart +++ b/lib/flutter_datetime_picker.dart @@ -34,7 +34,7 @@ class DatePicker { }) async { return await Navigator.push( context, - DatePickerRoute( + _DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -69,7 +69,7 @@ class DatePicker { }) async { return await Navigator.push( context, - DatePickerRoute( + _DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -102,7 +102,7 @@ class DatePicker { }) async { return await Navigator.push( context, - DatePickerRoute( + _DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -136,7 +136,7 @@ class DatePicker { }) async { return await Navigator.push( context, - DatePickerRoute( + _DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -170,7 +170,7 @@ class DatePicker { }) async { return await Navigator.push( context, - DatePickerRoute( + _DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -185,8 +185,8 @@ class DatePicker { } } -class DatePickerRoute extends PopupRoute { - DatePickerRoute({ +class _DatePickerRoute extends PopupRoute { + _DatePickerRoute({ this.showTitleActions, this.onChanged, this.onConfirm, @@ -257,7 +257,7 @@ class DatePickerWidget extends StatelessWidget { this.locale = LocaleType.en, this.pickerModel, this.theme, - }) : route = DatePickerRoute( + }) : route = _DatePickerRoute( showTitleActions: showTitleActions, onChanged: onChanged, onConfirm: onConfirm, @@ -275,7 +275,7 @@ class DatePickerWidget extends StatelessWidget { final LocaleType locale; final BasePickerModel? pickerModel; final DatePickerTheme? theme; - final DatePickerRoute route; + final _DatePickerRoute route; @override Widget build(BuildContext context) { @@ -304,7 +304,7 @@ class DatePickerComponent extends StatelessWidget { final DateChangedCallback? onChanged; - final DatePickerRoute route; + final _DatePickerRoute route; final LocaleType? locale; @@ -353,7 +353,7 @@ class _RenderPickerView extends StatelessWidget { required this.titleActionsView, }) : super(key: key); - final DatePickerRoute route; + final _DatePickerRoute route; final _ItemView itemView; final _TitleActionsView titleActionsView; @@ -596,7 +596,7 @@ class _TitleActionsView extends StatelessWidget { Key? key, required this.route, }) : super(key: key); - final DatePickerRoute route; + final _DatePickerRoute route; @override Widget build(BuildContext context) { From d6cae76f11b0d4ff288d8c7e48334530e704beae Mon Sep 17 00:00:00 2001 From: appvinio-filip-gawel Date: Mon, 16 Aug 2021 23:54:56 +0200 Subject: [PATCH 3/3] make private: _DatePickerComponent --- lib/flutter_datetime_picker.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/flutter_datetime_picker.dart b/lib/flutter_datetime_picker.dart index 3a0c94fa..1b50d44b 100644 --- a/lib/flutter_datetime_picker.dart +++ b/lib/flutter_datetime_picker.dart @@ -236,7 +236,7 @@ class _DatePickerRoute extends PopupRoute { Widget bottomSheet = MediaQuery.removePadding( context: context, removeTop: true, - child: DatePickerComponent( + child: _DatePickerComponent( onChanged: onChanged, locale: this.locale, route: this, @@ -293,8 +293,8 @@ class DatePickerWidget extends StatelessWidget { } } -class DatePickerComponent extends StatelessWidget { - DatePickerComponent({ +class _DatePickerComponent extends StatelessWidget { + _DatePickerComponent({ Key? key, required this.route, required this.pickerModel,