Created
December 16, 2020 00:05
-
-
Save avioli/4da113f5cb7f5bbb4730545becfc5127 to your computer and use it in GitHub Desktop.
An example of a CupertinoDatePicker widget with a controller
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:flutter/cupertino.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/scheduler.dart'; | |
void main() => runApp(MyApp()); | |
class MyApp extends StatefulWidget { | |
_MyAppState createState() => _MyAppState(); | |
} | |
class _MyAppState extends State<MyApp> { | |
DateTime _initialDate = | |
DateTime.now().subtract(Duration(days: 20, hours: 10, minutes: 5)); | |
DateTime _currentDate; | |
DateTime _currentDateTime; | |
DateTime _currentTime; | |
final _controller = CupertinoDatePickerController(); | |
@override | |
void initState() { | |
super.initState(); | |
// NOTE: _controller.selectedDate can only be used if the controller is used with a _single_ ControlledCupertinoDatePicker | |
// _controller.addListener(() { | |
// print('changed date: ${_controller.selectedDate}'); | |
// }); | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return CupertinoApp( | |
debugShowCheckedModeBanner: false, | |
home: CupertinoPageScaffold( | |
child: Form( | |
child: CustomScrollView( | |
slivers: <Widget>[ | |
CupertinoSliverNavigationBar( | |
largeTitle: const Text('Demo'), | |
trailing: Row( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
CupertinoButton( | |
padding: EdgeInsets.zero, | |
child: const Text('Now'), | |
onPressed: () { | |
_controller.scrollToDate(DateTime.now()); | |
}, | |
), | |
SizedBox(width: 8), | |
CupertinoButton( | |
padding: EdgeInsets.zero, | |
child: const Text('Reset'), | |
onPressed: () async { | |
print('will reset'); | |
await _controller.scrollToDate(_initialDate); | |
print('did reset'); | |
}, | |
), | |
], | |
), | |
), | |
SliverSafeArea( | |
top: false, | |
minimum: const EdgeInsets.only(top: 8), | |
sliver: SliverList( | |
delegate: SliverChildListDelegate.fixed([ | |
Column( | |
mainAxisSize: MainAxisSize.min, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 8), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
Text('Date'), | |
Text('$_currentDate', | |
maxLines: 1, | |
style: Theme.of(context).textTheme.caption), | |
], | |
), | |
), | |
Divider(color: CupertinoColors.lightBackgroundGray), | |
], | |
), | |
LimitedBox( | |
maxHeight: 200, | |
child: ControlledCupertinoDatePicker( | |
controller: _controller, | |
initialDateTime: _initialDate, | |
mode: CupertinoDatePickerMode.date, | |
onDateTimeChanged: (DateTime newDate) { | |
setState(() => _currentDate = newDate); | |
}, | |
), | |
), | |
Column( | |
mainAxisSize: MainAxisSize.min, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 8), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
Text('Date & time'), | |
Text('$_currentDateTime', | |
maxLines: 1, | |
style: Theme.of(context).textTheme.caption), | |
], | |
), | |
), | |
Divider(color: CupertinoColors.lightBackgroundGray), | |
], | |
), | |
LimitedBox( | |
maxHeight: 200, | |
child: ControlledCupertinoDatePicker( | |
controller: _controller, | |
initialDateTime: _initialDate, | |
mode: CupertinoDatePickerMode.dateAndTime, | |
onDateTimeChanged: (DateTime newDate) { | |
setState(() => _currentDateTime = newDate); | |
}, | |
), | |
), | |
Column( | |
mainAxisSize: MainAxisSize.min, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 8), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
Text('Time'), | |
Text('$_currentTime', | |
maxLines: 1, | |
style: Theme.of(context).textTheme.caption), | |
], | |
), | |
), | |
Divider(color: CupertinoColors.lightBackgroundGray), | |
], | |
), | |
LimitedBox( | |
maxHeight: 200, | |
child: ControlledCupertinoDatePicker( | |
controller: _controller, | |
initialDateTime: _initialDate, | |
mode: CupertinoDatePickerMode.time, | |
onDateTimeChanged: (DateTime newDate) { | |
setState(() => _currentTime = newDate); | |
}, | |
), | |
), | |
]), | |
), | |
), | |
], | |
), | |
), | |
), | |
); | |
} | |
} | |
// ///////////////////////////////////////////////////////////////////////////// | |
class CupertinoDatePickerController extends ChangeNotifier { | |
List<_ScrollStateLinker> _linkers = []; | |
void attach(_ScrollStateLinker linker) { | |
assert(!_linkers.contains(linker)); | |
_linkers.add(linker); | |
linker.addListener(notifyListeners); | |
} | |
void detach(_ScrollStateLinker linker) { | |
assert(_linkers.contains(linker)); | |
_linkers.remove(linker); | |
linker.removeListener(notifyListeners); | |
} | |
@override | |
void dispose() { | |
for (int i = 0; i < _linkers.length; i += 1) { | |
_linkers[i].removeListener(notifyListeners); | |
} | |
super.dispose(); | |
} | |
bool get hasClients => _linkers.isNotEmpty; | |
DateTime get selectedDate { | |
assert(_linkers.isNotEmpty, | |
'CupertinoDatePickerController not attached to any views.'); | |
assert(_linkers.length == 1, | |
'CupertinoDatePickerController attached to multiple views.'); | |
return _linkers.single.selectedDate; | |
} | |
Future<void> scrollToDate(DateTime newDate) { | |
final futures = <Future<void>>[]; | |
for (int i = 0; i < _linkers.length; i += 1) { | |
futures.add(_linkers[i].scrollToDate(newDate)); | |
} | |
return Future.wait(futures); | |
} | |
_ScrollStateLinker _createScrollStateLinker( | |
_ControlledCupertinoDatePickerState context) { | |
return _ScrollStateLinker(context: context); | |
} | |
} | |
// ///////////////////////////////////////////////////////////////////////////// | |
class ControlledCupertinoDatePicker extends CupertinoDatePicker { | |
ControlledCupertinoDatePicker({ | |
Key key, | |
this.controller, | |
CupertinoDatePickerMode mode = CupertinoDatePickerMode.dateAndTime, | |
ValueChanged<DateTime> onDateTimeChanged, | |
DateTime initialDateTime, | |
DateTime minimumDate, | |
DateTime maximumDate, | |
int minimumYear = 1, | |
int maximumYear, | |
int minuteInterval = 1, | |
bool use24hFormat = false, | |
Color backgroundColor, | |
}) : assert(controller != null || onDateTimeChanged != null, | |
'either a controller or onDateTimeChanged must be set'), | |
super( | |
key: key, | |
mode: mode, | |
onDateTimeChanged: onDateTimeChanged, | |
initialDateTime: initialDateTime, | |
minimumDate: minimumDate, | |
maximumDate: maximumDate, | |
minimumYear: minimumYear, | |
maximumYear: maximumYear, | |
minuteInterval: minuteInterval, | |
use24hFormat: use24hFormat, | |
backgroundColor: backgroundColor, | |
); | |
final CupertinoDatePickerController controller; | |
State<StatefulWidget> createState() => _ControlledCupertinoDatePickerState(); | |
} | |
// ///////////////////////////////////////////////////////////////////////////// | |
class _ControlledCupertinoDatePickerState | |
extends State<ControlledCupertinoDatePicker> { | |
GlobalKey _key = GlobalKey(); | |
_ScrollStateLinker _linker; | |
@override | |
void initState() { | |
super.initState(); | |
_linker = widget.controller?._createScrollStateLinker(this); | |
widget.controller?.attach(_linker); | |
} | |
@override | |
void didUpdateWidget(ControlledCupertinoDatePicker oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget.controller != oldWidget.controller) { | |
if (_linker != null) oldWidget.controller?.detach(_linker); | |
_linker = widget.controller?._createScrollStateLinker(this); | |
widget.controller?.attach(_linker); | |
} | |
} | |
@override | |
void dispose() { | |
if (_linker != null) { | |
widget.controller?.detach(_linker); | |
_linker.dispose(); | |
} | |
super.dispose(); | |
} | |
Widget build(BuildContext context) { | |
return CupertinoDatePicker( | |
key: _key, | |
mode: widget.mode, | |
onDateTimeChanged: _onDateTimeChanged, | |
initialDateTime: widget.initialDateTime, | |
minimumDate: widget.minimumDate, | |
maximumDate: widget.maximumDate, | |
minimumYear: widget.minimumYear, | |
maximumYear: widget.maximumYear, | |
minuteInterval: widget.minuteInterval, | |
use24hFormat: widget.use24hFormat, | |
backgroundColor: widget.backgroundColor, | |
); | |
} | |
DateTime get selectedDate { | |
dynamic _state = _key.currentState as dynamic; | |
switch (widget.mode) { | |
case CupertinoDatePickerMode.date: | |
return DateTime( | |
_state.selectedYear as int, | |
_state.selectedMonth as int, | |
_state.selectedDay as int, | |
); | |
case CupertinoDatePickerMode.time: | |
case CupertinoDatePickerMode.dateAndTime: | |
return _state.selectedDateTime as DateTime; | |
} | |
return _state.selectedDateTime as DateTime; | |
} | |
Future<void> scrollToDate(DateTime newDate) { | |
switch (widget.mode) { | |
case CupertinoDatePickerMode.date: | |
return _scrollToSafeDate(newDate); | |
break; | |
case CupertinoDatePickerMode.time: | |
case CupertinoDatePickerMode.dateAndTime: | |
return _scrollToSafeDateTime(newDate); | |
} | |
assert(false); | |
return _scrollToSafeDateTime(newDate); | |
} | |
void _onDateTimeChanged(DateTime newDate) { | |
widget.onDateTimeChanged?.call(newDate); | |
_linker?.didUpdateValue(); | |
} | |
Future<void> _scrollToSafeDate(DateTime newDate) { | |
final maxSelectDate = newDate.add(const Duration(days: 1)); | |
final minCheck = widget.minimumDate?.isBefore(maxSelectDate) ?? true; | |
final maxCheck = widget.maximumDate?.isBefore(newDate) ?? false; | |
if (!minCheck || maxCheck) { | |
// We have minCheck === !maxCheck. | |
final targetDate = minCheck ? widget.maximumDate : widget.minimumDate; | |
return _scrollToDate(targetDate); | |
} | |
return _scrollToDate(newDate); | |
} | |
Future<void> _scrollToDate(DateTime newDate) { | |
assert(newDate != null); | |
final completer = Completer<void>(); | |
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { | |
dynamic _state = _key.currentState as dynamic; | |
final futures = <Future<void>>[]; | |
if (_state.selectedYear != newDate.year) { | |
futures.add(_animateColumnControllerToItem( | |
_state.yearController, newDate.year)); | |
} | |
if (_state.selectedMonth != newDate.month) { | |
futures.add(_animateColumnControllerToItem( | |
_state.monthController, newDate.month - 1)); | |
} | |
if (_state.selectedDay != newDate.day) { | |
futures.add(_animateColumnControllerToItem( | |
_state.dayController, newDate.day - 1)); | |
} | |
Future.wait(futures).whenComplete(completer.complete); | |
}); | |
return completer.future; | |
} | |
Future<void> _scrollToSafeDateTime(DateTime newDate) { | |
final minCheck = widget.minimumDate?.isAfter(newDate) ?? false; | |
final maxCheck = widget.maximumDate?.isBefore(newDate) ?? false; | |
if (minCheck || maxCheck) { | |
// We have minCheck === !maxCheck. | |
final targetDate = minCheck ? widget.minimumDate : widget.maximumDate; | |
return _scrollToDateTime(targetDate); | |
} | |
return _scrollToDateTime(newDate); | |
} | |
Future<void> _scrollToDateTime(DateTime newDate) { | |
assert(newDate != null); | |
final completer = Completer<void>(); | |
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { | |
dynamic _state = _key.currentState as dynamic; | |
final fromDate = _state.selectedDateTime as DateTime; | |
final futures = <Future<void>>[]; | |
if (fromDate.year != newDate.year || | |
fromDate.month != newDate.month || | |
fromDate.day != newDate.day) { | |
final diff = newDate.difference(widget.initialDateTime); | |
futures.add( | |
_animateColumnControllerToItem(_state.dateController, diff.inDays)); | |
} | |
if (fromDate.hour != newDate.hour) { | |
final bool needsMeridiemChange = | |
!widget.use24hFormat && fromDate.hour ~/ 12 != newDate.hour ~/ 12; | |
// In AM/PM mode, the pickers should not scroll all the way to the other hour region. | |
if (needsMeridiemChange) { | |
futures.add(_animateColumnControllerToItem(_state.meridiemController, | |
1 - _state.meridiemController.selectedItem)); | |
// Keep the target item index in the current 12-h region. | |
final int newItem = (_state.hourController.selectedItem ~/ 12) * 12 + | |
(_state.hourController.selectedItem + | |
newDate.hour - | |
fromDate.hour) % | |
12; | |
futures.add( | |
_animateColumnControllerToItem(_state.hourController, newItem)); | |
} else { | |
futures.add(_animateColumnControllerToItem( | |
_state.hourController, | |
_state.hourController.selectedItem + newDate.hour - fromDate.hour, | |
)); | |
} | |
} | |
if (fromDate.minute != newDate.minute) { | |
futures.add(_animateColumnControllerToItem( | |
_state.minuteController, newDate.minute)); | |
} | |
Future.wait(futures).whenComplete(completer.complete); | |
}); | |
return completer.future; | |
} | |
} | |
// ///////////////////////////////////////////////////////////////////////////// | |
Future<void> _animateColumnControllerToItem( | |
FixedExtentScrollController controller, | |
int targetItem, | |
) { | |
return controller.animateToItem( | |
targetItem, | |
curve: Curves.easeInOut, | |
duration: const Duration(milliseconds: 200), | |
); | |
} | |
// ///////////////////////////////////////////////////////////////////////////// | |
class _ScrollStateLinker extends ChangeNotifier { | |
_ScrollStateLinker({@required this.context}); | |
final _ControlledCupertinoDatePickerState context; | |
DateTime get selectedDate => context.selectedDate; | |
Future<void> scrollToDate(DateTime date) => context.scrollToDate(date); | |
void didUpdateValue() { | |
notifyListeners(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment