2022-11-16 17:01:05 +01:00
|
|
|
// SPDX-FileCopyrightText: 2022 Iconica
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_date_time_picker/src/models/date_time_picker_theme.dart';
|
|
|
|
import 'package:flutter_date_time_picker/src/utils/date_time_picker_controller.dart';
|
2022-11-18 15:57:03 +01:00
|
|
|
import 'package:flutter_date_time_picker/src/models/date_constraint.dart';
|
2022-11-16 17:01:05 +01:00
|
|
|
import 'package:flutter_date_time_picker/src/widgets/overlay_date_time_picker/overlay.dart';
|
|
|
|
|
|
|
|
class OverlayDateTimePicker extends StatefulWidget {
|
|
|
|
const OverlayDateTimePicker({
|
|
|
|
this.theme = const DateTimePickerTheme(),
|
2022-11-18 11:39:26 +01:00
|
|
|
this.textStyle = const TextStyle(),
|
2022-11-16 17:01:05 +01:00
|
|
|
this.alignment = Alignment.bottomRight,
|
|
|
|
this.initialDate,
|
2022-11-18 15:57:03 +01:00
|
|
|
this.size = const Size(325, 375),
|
2022-11-16 17:01:05 +01:00
|
|
|
this.onTapDay,
|
|
|
|
this.highlightToday = true,
|
|
|
|
this.alwaysUse24HourFormat = true,
|
|
|
|
this.pickTime = false,
|
|
|
|
this.markedDates,
|
|
|
|
this.disabledDates,
|
|
|
|
this.disabledTimes,
|
|
|
|
this.child,
|
|
|
|
super.key,
|
|
|
|
this.buttonBuilder,
|
|
|
|
this.closeOnSelectDate = true,
|
2022-11-18 11:39:26 +01:00
|
|
|
this.showWeekDays = true,
|
2022-11-18 15:57:03 +01:00
|
|
|
this.dateTimeConstraint = const DateTimeConstraint(),
|
2022-11-21 14:53:07 +01:00
|
|
|
this.onNextPageButtonBuilder,
|
|
|
|
this.onPreviousPageButtonBuilder,
|
2022-11-16 17:01:05 +01:00
|
|
|
}) : assert(child != null || buttonBuilder != null);
|
|
|
|
|
|
|
|
/// The child contained by the DatePicker.
|
|
|
|
final Widget? child;
|
|
|
|
|
|
|
|
/// Visual properties for the [OverlayDateTimePicker]
|
|
|
|
final DateTimePickerTheme theme;
|
|
|
|
|
2022-11-18 11:39:26 +01:00
|
|
|
/// Style to base the text on
|
|
|
|
final TextStyle textStyle;
|
|
|
|
|
2022-11-16 17:01:05 +01:00
|
|
|
/// Callback that provides the date tapped on as a [DateTime] object.
|
|
|
|
final Function(DateTime date)? onTapDay;
|
|
|
|
|
|
|
|
/// Whether the current day should be highlighted in the [OverlayDateTimePicker]
|
|
|
|
final bool highlightToday;
|
|
|
|
|
|
|
|
/// a [bool] to set de clock on [TimePickerDialog] to a fixed 24 or 12-hour format.
|
|
|
|
final bool alwaysUse24HourFormat;
|
|
|
|
|
2022-11-18 15:57:03 +01:00
|
|
|
/// is a [bool] that determines if the user is able to pick a time after picking a date using the [TimePickerDialog].
|
2022-11-16 17:01:05 +01:00
|
|
|
final bool pickTime;
|
|
|
|
|
|
|
|
/// indicates the starting date. Default is [DateTime.now()]
|
|
|
|
final DateTime? initialDate;
|
|
|
|
|
2022-11-18 15:57:03 +01:00
|
|
|
/// contain the dates [DateTime] that will be marked in the [OverlayDateTimePicker] by a small dot.
|
2022-11-16 17:01:05 +01:00
|
|
|
final List<DateTime>? markedDates;
|
|
|
|
|
|
|
|
/// a [List] of [DateTime] objects that will be disabled and cannot be interacted with whatsoever.
|
|
|
|
final List<DateTime>? disabledDates;
|
|
|
|
|
|
|
|
/// a [List] of [TimeOfDay] objects that cannot be picked in the [TimePickerDialog].
|
|
|
|
final List<TimeOfDay>? disabledTimes;
|
|
|
|
|
|
|
|
/// an [Alignment] to align the overlay relative to the button
|
|
|
|
final Alignment alignment;
|
|
|
|
|
|
|
|
/// a [Size] that indicates the size of the overlay
|
|
|
|
final Size size;
|
|
|
|
|
|
|
|
/// [buttonBuilder] is a method for building the button that can trigger the overlay to appear
|
|
|
|
final Widget Function(Key key, void Function() onPressed)? buttonBuilder;
|
|
|
|
|
2022-11-18 15:57:03 +01:00
|
|
|
/// is a [bool] that indicates if the overlay should be closed if a date has been picked.
|
2022-11-16 17:01:05 +01:00
|
|
|
final bool closeOnSelectDate;
|
|
|
|
|
2022-11-18 11:39:26 +01:00
|
|
|
/// [showWeekDays] is a [bool] that determines if day in the week indicators should be shown
|
|
|
|
final bool showWeekDays;
|
|
|
|
|
2022-11-18 15:57:03 +01:00
|
|
|
/// a [DateTimeConstraint] that dictates the constraints of the dates that can be picked.
|
|
|
|
final DateTimeConstraint dateTimeConstraint;
|
|
|
|
|
2022-11-21 14:53:07 +01:00
|
|
|
/// a [Function] that determents the icon of the button for going to the next page
|
|
|
|
final Widget Function(void Function()? onPressed)? onNextPageButtonBuilder;
|
2022-11-18 15:57:03 +01:00
|
|
|
|
2022-11-21 14:53:07 +01:00
|
|
|
/// a [Function] that determents the icon of the button for going to the previous page
|
|
|
|
final Widget Function(void Function()? onPressed)?
|
|
|
|
onPreviousPageButtonBuilder;
|
2022-11-18 15:57:03 +01:00
|
|
|
|
2022-11-16 17:01:05 +01:00
|
|
|
@override
|
|
|
|
State<OverlayDateTimePicker> createState() => _OverlayDateTimePickerState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _OverlayDateTimePickerState extends State<OverlayDateTimePicker> {
|
|
|
|
final GlobalKey buttonKey = GlobalKey(
|
|
|
|
debugLabel: "Overlay Date Time Picker - Button",
|
|
|
|
);
|
|
|
|
|
|
|
|
bool _isShown = false;
|
|
|
|
|
2022-12-14 15:06:42 +01:00
|
|
|
_DropdownRoute? _dropdownRoute;
|
|
|
|
|
2022-11-16 17:01:05 +01:00
|
|
|
late final DateTimePickerController _dateTimePickerController =
|
|
|
|
DateTimePickerController(
|
|
|
|
highlightToday: widget.highlightToday,
|
|
|
|
alwaysUse24HourFormat: widget.alwaysUse24HourFormat,
|
|
|
|
pickTime: widget.pickTime,
|
|
|
|
theme: widget.theme,
|
|
|
|
markedDates: widget.markedDates,
|
|
|
|
disabledDates: widget.disabledDates,
|
|
|
|
disabledTimes: widget.disabledTimes,
|
|
|
|
onTapDayCallBack: (date) {
|
|
|
|
widget.onTapDay?.call(date);
|
|
|
|
if (widget.closeOnSelectDate) {
|
2022-12-14 15:06:42 +01:00
|
|
|
Navigator.of(context).pop();
|
2022-11-16 17:01:05 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
browsingDate: widget.initialDate ?? DateTime.now(),
|
|
|
|
selectedDate: widget.initialDate ?? DateTime.now(),
|
|
|
|
);
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
_dateTimePickerController.dispose();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
2022-12-14 15:06:42 +01:00
|
|
|
void _onPressed() async {
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
|
|
_isShown = !_isShown;
|
|
|
|
});
|
|
|
|
final TextDirection? textDirection = Directionality.maybeOf(context);
|
|
|
|
|
|
|
|
final NavigatorState navigator = Navigator.of(context);
|
|
|
|
|
|
|
|
final RenderBox buttonBox = context.findRenderObject()! as RenderBox;
|
|
|
|
final Rect buttonRect = buttonBox.localToGlobal(Offset.zero,
|
|
|
|
ancestor: navigator.context.findRenderObject()) &
|
|
|
|
buttonBox.size;
|
|
|
|
|
|
|
|
_dropdownRoute = _DropdownRoute(
|
|
|
|
child: _buildOverlay(context),
|
|
|
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
|
|
|
buttonRect:
|
|
|
|
EdgeInsets.zero.resolve(textDirection).inflateRect(buttonRect),
|
|
|
|
alignment: widget.alignment,
|
|
|
|
menuSize: widget.size,
|
|
|
|
);
|
|
|
|
await navigator.push(_dropdownRoute!);
|
|
|
|
if (!mounted) {
|
|
|
|
return;
|
2022-11-16 17:01:05 +01:00
|
|
|
}
|
2022-12-14 15:06:42 +01:00
|
|
|
|
|
|
|
_close();
|
2022-11-16 17:01:05 +01:00
|
|
|
}
|
|
|
|
|
2022-12-14 15:06:42 +01:00
|
|
|
void _close() {
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
|
|
_isShown = false;
|
|
|
|
});
|
2022-11-16 17:01:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
if (widget.buttonBuilder != null) {
|
|
|
|
return widget.buttonBuilder!.call(
|
|
|
|
buttonKey,
|
|
|
|
() {
|
2022-12-14 15:06:42 +01:00
|
|
|
_onPressed();
|
2022-11-16 17:01:05 +01:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return ElevatedButton(
|
|
|
|
key: buttonKey,
|
|
|
|
onPressed: () {
|
2022-12-14 15:06:42 +01:00
|
|
|
_onPressed();
|
2022-11-16 17:01:05 +01:00
|
|
|
},
|
|
|
|
child: widget.child);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildOverlay(BuildContext context) {
|
|
|
|
return Container(
|
2022-11-22 11:50:40 +01:00
|
|
|
decoration: (widget.theme.shapeBorder == null)
|
|
|
|
? BoxDecoration(
|
|
|
|
color: widget.theme.backgroundColor,
|
|
|
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
|
|
boxShadow: [
|
|
|
|
BoxShadow(
|
|
|
|
blurRadius: 5,
|
|
|
|
color: Colors.black.withOpacity(0.25),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
: ShapeDecoration(
|
|
|
|
shape: widget.theme.shapeBorder!,
|
|
|
|
color: widget.theme.backgroundColor,
|
|
|
|
shadows: [
|
|
|
|
BoxShadow(
|
|
|
|
blurRadius: 5,
|
|
|
|
color: Colors.black.withOpacity(0.25),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
2022-11-16 17:01:05 +01:00
|
|
|
child: SizedBox(
|
|
|
|
width: widget.size.width,
|
|
|
|
height: widget.size.height,
|
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.all(8.0),
|
|
|
|
child: OverlayDateTimeContent(
|
|
|
|
theme: widget.theme,
|
2022-11-18 11:39:26 +01:00
|
|
|
textStyle: widget.textStyle,
|
2022-11-16 17:01:05 +01:00
|
|
|
size: widget.size,
|
|
|
|
controller: _dateTimePickerController,
|
2022-11-18 11:39:26 +01:00
|
|
|
showWeekDays: true,
|
2022-11-16 17:01:05 +01:00
|
|
|
onNextDate: nextDate,
|
|
|
|
onPreviousDate: previousDate,
|
2022-11-18 15:57:03 +01:00
|
|
|
dateTimeConstraint: widget.dateTimeConstraint,
|
2022-11-21 14:53:07 +01:00
|
|
|
onNextPageButtonChild: widget.onNextPageButtonBuilder,
|
|
|
|
onPreviousPageButtonChild: widget.onPreviousPageButtonBuilder,
|
2022-11-16 17:01:05 +01:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void nextDate() {
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
2023-01-02 11:02:32 +01:00
|
|
|
_dateTimePickerController.browsingDate = DateTime(
|
|
|
|
_dateTimePickerController.browsingDate.year,
|
|
|
|
_dateTimePickerController.browsingDate.month + 1,
|
|
|
|
_dateTimePickerController.browsingDate.day,
|
2022-11-16 17:01:05 +01:00
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void previousDate() {
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
2023-01-02 11:02:32 +01:00
|
|
|
_dateTimePickerController.browsingDate = DateTime(
|
|
|
|
_dateTimePickerController.browsingDate.year,
|
|
|
|
_dateTimePickerController.browsingDate.month - 1,
|
|
|
|
_dateTimePickerController.browsingDate.day,
|
2022-11-16 17:01:05 +01:00
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2022-12-14 15:06:42 +01:00
|
|
|
|
|
|
|
class _DropdownRoute extends PopupRoute<Widget> {
|
|
|
|
_DropdownRoute({
|
|
|
|
required this.barrierLabel,
|
|
|
|
required this.child,
|
|
|
|
required this.alignment,
|
|
|
|
required this.menuSize,
|
|
|
|
required this.buttonRect,
|
|
|
|
});
|
|
|
|
|
|
|
|
final Widget child;
|
|
|
|
final Alignment alignment;
|
|
|
|
final Size menuSize;
|
|
|
|
final Rect buttonRect;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Color? get barrierColor => null;
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool get barrierDismissible => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
final String? barrierLabel;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget buildPage(BuildContext context, Animation<double> animation,
|
|
|
|
Animation<double> secondaryAnimation) {
|
|
|
|
return CustomSingleChildLayout(
|
|
|
|
delegate: _DropdownMenuLayoutDelegate(
|
|
|
|
buttonRect: buttonRect,
|
|
|
|
menuSize: menuSize,
|
|
|
|
alignment: alignment,
|
|
|
|
),
|
|
|
|
child: Material(child: child),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Duration get transitionDuration => const Duration(milliseconds: 100);
|
|
|
|
|
|
|
|
void dismiss() {
|
|
|
|
navigator?.pop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _DropdownMenuLayoutDelegate extends SingleChildLayoutDelegate {
|
|
|
|
_DropdownMenuLayoutDelegate({
|
|
|
|
required this.buttonRect,
|
|
|
|
required this.menuSize,
|
|
|
|
required this.alignment,
|
|
|
|
});
|
|
|
|
|
|
|
|
final Rect buttonRect;
|
|
|
|
final Size menuSize;
|
|
|
|
final Alignment alignment;
|
|
|
|
|
|
|
|
@override
|
|
|
|
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
|
|
|
return BoxConstraints(
|
|
|
|
minWidth: menuSize.width,
|
|
|
|
maxWidth: menuSize.width,
|
|
|
|
minHeight: menuSize.height,
|
|
|
|
maxHeight: menuSize.height,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Offset getPositionForChild(Size size, Size childSize) {
|
|
|
|
Rect pop = Offset.zero & childSize;
|
|
|
|
pop.center;
|
|
|
|
return buttonRect.center -
|
|
|
|
pop.center +
|
|
|
|
Offset(
|
|
|
|
(childSize.width + buttonRect.width) * 0.5 * alignment.x,
|
|
|
|
(childSize.height + buttonRect.height) * 0.5 * alignment.y,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool shouldRelayout(_DropdownMenuLayoutDelegate oldDelegate) {
|
|
|
|
return buttonRect != oldDelegate.buttonRect;
|
|
|
|
}
|
|
|
|
}
|