Skip to content

Instantly share code, notes, and snippets.

@avioli
Created December 16, 2020 00:05
Show Gist options
  • Save avioli/4da113f5cb7f5bbb4730545becfc5127 to your computer and use it in GitHub Desktop.
Save avioli/4da113f5cb7f5bbb4730545becfc5127 to your computer and use it in GitHub Desktop.
An example of a CupertinoDatePicker widget with a controller
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