Files
flutter_common/lib/calendarcalendar/custom_multiple_choose_date_picker.dart

1094 lines
36 KiB
Dart
Executable File

import 'dart:math' as math;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:sticky_headers/sticky_headers.dart';
///自定义日期区间选择
class CustomMultipleChooseDatePicker extends StatefulWidget {
CustomMultipleChooseDatePicker({
Key? key,
DateTime? initialStartDate,
DateTime? initialEndDate,
required DateTime firstDate,
required DateTime lastDate,
required this.onDateSelected,
DateTime? currentDate,
this.selectedDateDecoration,
this.currentDateDecoration,
this.rangeBackgroundColor,
this.weekendTextStyle,
this.weekTextStyle,
this.yearTextStyle,
}) : initialStartDate = initialStartDate != null
? DateUtils.dateOnly(initialStartDate)
: null,
initialEndDate =
initialEndDate != null ? DateUtils.dateOnly(initialEndDate) : null,
assert(firstDate != null),
assert(lastDate != null),
firstDate = DateUtils.dateOnly(firstDate),
lastDate = DateUtils.dateOnly(lastDate),
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
super(key: key) {
assert(
this.initialStartDate == null ||
this.initialEndDate == null ||
!this.initialStartDate!.isAfter(initialEndDate!),
'initialStartDate must be on or before initialEndDate.',
);
assert(
!this.lastDate.isBefore(this.firstDate),
'firstDate must be on or before lastDate.',
);
}
final DateTime? initialStartDate;
final DateTime? initialEndDate;
final DateTime firstDate;
final DateTime lastDate;
final DateTime currentDate;
final Function(DateTime? startDate, DateTime? endDate) onDateSelected;
///开始时间和结束时间按钮样式
final BoxDecoration? selectedDateDecoration;
///当前时间样式
final BoxDecoration? currentDateDecoration;
///选中时间区间的背景颜色
final Color? rangeBackgroundColor;
///周六周天字体颜色
final TextStyle? weekendTextStyle;
///周字体颜色
final TextStyle? weekTextStyle;
///年月的字体
final TextStyle? yearTextStyle;
@override
CustomMultipleChooseDatePickerState createState() =>
CustomMultipleChooseDatePickerState();
}
class CustomMultipleChooseDatePickerState
extends State<CustomMultipleChooseDatePicker> {
final GlobalKey _scrollViewKey = GlobalKey();
DateTime? _startDate;
DateTime? _endDate;
int _initialMonthIndex = 0;
late ScrollController _controller;
late bool _showWeekBottomDivider;
@override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_scrollListener);
_startDate = widget.initialStartDate;
_endDate = widget.initialEndDate;
// Calculate the index for the initially displayed month. This is needed to
// divide the list of months into two `SliverList`s.
final DateTime initialDate = widget.initialStartDate ?? widget.currentDate;
if (!initialDate.isBefore(widget.firstDate) &&
!initialDate.isAfter(widget.lastDate)) {
_initialMonthIndex = DateUtils.monthDelta(widget.firstDate, initialDate);
}
_showWeekBottomDivider = _initialMonthIndex != 0;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _scrollListener() {
if (_controller.offset <= _controller.position.minScrollExtent) {
setState(() {
_showWeekBottomDivider = false;
});
} else if (!_showWeekBottomDivider) {
setState(() {
_showWeekBottomDivider = true;
});
}
}
int get _numberOfMonths =>
DateUtils.monthDelta(widget.firstDate, widget.lastDate) + 1;
void _vibrate() {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
HapticFeedback.vibrate();
break;
default:
break;
}
}
// This updates the selected date range using this logic:
//
// * From the unselected state, selecting one date creates the start date.
// * If the next selection is before the start date, reset date range and
// set the start date to that selection.
// * If the next selection is on or after the start date, set the end date
// to that selection.
// * After both start and end dates are selected, any subsequent selection
// resets the date range and sets start date to that selection.
void _updateSelection(DateTime date) {
_vibrate();
setState(() {
if (_startDate != null &&
_endDate == null &&
!date.isBefore(_startDate!)) {
_endDate = date;
} else {
_startDate = date;
if (_endDate != null) {
_endDate = null;
}
}
});
widget.onDateSelected(_startDate ?? null, _endDate ?? null);
}
Widget _buildMonthItem(
BuildContext context, int index, bool beforeInitialMonth) {
final int monthIndex = beforeInitialMonth
? _initialMonthIndex - index - 1
: _initialMonthIndex + index;
final DateTime month =
DateUtils.addMonthsToMonthDate(widget.firstDate, monthIndex);
return _MonthItem(
selectedDateStart: _startDate,
selectedDateEnd: _endDate,
currentDate: widget.currentDate,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
displayedMonth: month,
onChanged: _updateSelection,
selectedDateDecoration: widget.selectedDateDecoration,
currentDateDecoration: widget.currentDateDecoration,
rangeBackgroundColor: widget.rangeBackgroundColor);
}
///年月展示
Widget _yearMonthWidget(int index, bool beforeInitialMonth) {
final int monthIndex = beforeInitialMonth
? _initialMonthIndex - index - 1
: _initialMonthIndex + index;
final DateTime month =
DateUtils.addMonthsToMonthDate(widget.firstDate, monthIndex);
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 7),
alignment: AlignmentDirectional.centerStart,
color: const Color(0xffF8F8F8),
child: ExcludeSemantics(
child: Text(
localizations.formatMonthYear(month),
style: widget.yearTextStyle ??
textTheme.bodyMedium!
.apply(color: themeData.colorScheme.onSurface),
),
),
);
}
@override
Widget build(BuildContext context) {
const Key sliverAfterKey = Key('sliverAfterKey');
return Container(
color: Colors.white,
child: Column(
children: <Widget>[
_DayHeaders(
weekendTextStyle: widget.weekendTextStyle,
weekTextStyle: widget.weekTextStyle,
),
if (_showWeekBottomDivider) const Divider(height: 0),
Expanded(
child: _CalendarKeyboardNavigator(
firstDate: widget.firstDate,
lastDate: widget.lastDate,
initialFocusedDay:
_startDate ?? widget.initialStartDate ?? widget.currentDate,
// In order to prevent performance issues when displaying the
// correct initial month, 2 `SliverList`s are used to split the
// months. The first item in the second SliverList is the initial
// month to be displayed.
child: CustomScrollView(
key: _scrollViewKey,
controller: _controller,
center: sliverAfterKey,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => StickyHeader(
header: _yearMonthWidget(index, true),
content: _buildMonthItem(context, index, true),
),
childCount: _initialMonthIndex,
),
),
SliverList(
key: sliverAfterKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => StickyHeader(
header: _yearMonthWidget(index, false),
content: _buildMonthItem(context, index, false),
),
childCount: _numberOfMonths - _initialMonthIndex,
),
),
],
),
),
),
],
),
);
}
}
///月份选择
class _MonthItem extends StatefulWidget {
_MonthItem(
{Key? key,
required this.selectedDateStart,
required this.selectedDateEnd,
required this.currentDate,
required this.onChanged,
required this.firstDate,
required this.lastDate,
required this.displayedMonth,
this.dragStartBehavior = DragStartBehavior.start,
this.selectedDateDecoration,
this.currentDateDecoration,
this.rangeBackgroundColor})
: assert(firstDate != null),
assert(lastDate != null),
assert(!firstDate.isAfter(lastDate)),
assert(selectedDateStart == null ||
!selectedDateStart.isBefore(firstDate)),
assert(selectedDateEnd == null || !selectedDateEnd.isBefore(firstDate)),
assert(
selectedDateStart == null || !selectedDateStart.isAfter(lastDate)),
assert(selectedDateEnd == null || !selectedDateEnd.isAfter(lastDate)),
assert(selectedDateStart == null ||
selectedDateEnd == null ||
!selectedDateStart.isAfter(selectedDateEnd)),
assert(currentDate != null),
assert(onChanged != null),
assert(displayedMonth != null),
assert(dragStartBehavior != null),
super(key: key);
/// The currently selected start date.
///
/// This date is highlighted in the picker.
final DateTime? selectedDateStart;
/// The currently selected end date.
///
/// This date is highlighted in the picker.
final DateTime? selectedDateEnd;
/// The current date at the time the picker is displayed.
final DateTime currentDate;
/// Called when the user picks a day.
final ValueChanged<DateTime> onChanged;
/// The earliest date the user is permitted to pick.
final DateTime firstDate;
/// The latest date the user is permitted to pick.
final DateTime lastDate;
/// The month whose days are displayed by this picker.
final DateTime displayedMonth;
/// Determines the way that drag start behavior is handled.
///
/// If set to [DragStartBehavior.start], the drag gesture used to scroll a
/// date picker wheel will begin upon the detection of a drag gesture. If set
/// to [DragStartBehavior.down] it will begin when a down event is first
/// detected.
///
/// In general, setting this to [DragStartBehavior.start] will make drag
/// animation smoother and setting it to [DragStartBehavior.down] will make
/// drag behavior feel slightly more reactive.
///
/// By default, the drag start behavior is [DragStartBehavior.start].
///
/// See also:
///
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for
/// the different behaviors.
final DragStartBehavior dragStartBehavior;
final BoxDecoration? selectedDateDecoration;
final BoxDecoration? currentDateDecoration;
final Color? rangeBackgroundColor;
@override
_MonthItemState createState() => _MonthItemState();
}
class _MonthItemState extends State<_MonthItem> {
/// List of [FocusNode]s, one for each day of the month.
late List<FocusNode> _dayFocusNodes;
@override
void initState() {
super.initState();
final int daysInMonth = DateUtils.getDaysInMonth(
widget.displayedMonth.year, widget.displayedMonth.month);
_dayFocusNodes = List<FocusNode>.generate(
daysInMonth,
(int index) =>
FocusNode(skipTraversal: true, debugLabel: 'Day ${index + 1}'),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Check to see if the focused date is in this month, if so focus it.
final DateTime? focusedDate = _FocusedDate.of(context)?.date;
if (focusedDate != null &&
DateUtils.isSameMonth(widget.displayedMonth, focusedDate)) {
_dayFocusNodes[focusedDate.day - 1].requestFocus();
}
}
@override
void dispose() {
for (final FocusNode node in _dayFocusNodes) {
node.dispose();
}
super.dispose();
}
Color _highlightColor(BuildContext context) {
return widget.rangeBackgroundColor ??
Theme.of(context).colorScheme.primary.withOpacity(0.12);
}
void _dayFocusChanged(bool focused) {
if (focused) {
final TraversalDirection? focusDirection =
_FocusedDate.of(context)?.scrollDirection;
if (focusDirection != null) {
ScrollPositionAlignmentPolicy policy =
ScrollPositionAlignmentPolicy.explicit;
switch (focusDirection) {
case TraversalDirection.up:
case TraversalDirection.left:
policy = ScrollPositionAlignmentPolicy.keepVisibleAtStart;
break;
case TraversalDirection.right:
case TraversalDirection.down:
policy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd;
break;
}
Scrollable.ensureVisible(
primaryFocus!.context!,
duration: _monthScrollDuration,
alignmentPolicy: policy,
);
}
}
}
Widget _buildDayItem(BuildContext context, DateTime dayToBuild,
int firstDayOffset, int daysInMonth) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final TextTheme textTheme = theme.textTheme;
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final TextDirection textDirection = Directionality.of(context);
final Color highlightColor = _highlightColor(context);
final int day = dayToBuild.day;
final bool isDisabled = dayToBuild.isAfter(widget.lastDate) ||
dayToBuild.isBefore(widget.firstDate);
BoxDecoration? decoration;
TextStyle? itemStyle = textTheme.bodyMedium;
final bool isRangeSelected =
widget.selectedDateStart != null && widget.selectedDateEnd != null;
final bool isSelectedDayStart = widget.selectedDateStart != null &&
dayToBuild.isAtSameMomentAs(widget.selectedDateStart!);
final bool isSelectedDayEnd = widget.selectedDateEnd != null &&
dayToBuild.isAtSameMomentAs(widget.selectedDateEnd!);
final bool isInRange = isRangeSelected &&
dayToBuild.isAfter(widget.selectedDateStart!) &&
dayToBuild.isBefore(widget.selectedDateEnd!);
_HighlightPainter? highlightPainter;
if (isSelectedDayStart || isSelectedDayEnd) {
// The selected start and end dates gets a circle background
// highlight, and a contrasting text color.
itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.onPrimary);
decoration = widget.selectedDateDecoration ??
BoxDecoration(
color: colorScheme.primary,
shape: BoxShape.circle,
);
if (isRangeSelected &&
widget.selectedDateStart != widget.selectedDateEnd) {
final _HighlightPainterStyle style = isSelectedDayStart
? _HighlightPainterStyle.highlightTrailing
: _HighlightPainterStyle.highlightLeading;
highlightPainter = _HighlightPainter(
color: highlightColor,
style: style,
textDirection: textDirection,
);
}
} else if (isInRange) {
// The days within the range get a light background highlight.
highlightPainter = _HighlightPainter(
color: highlightColor,
style: _HighlightPainterStyle.highlightAll,
textDirection: textDirection,
);
} else if (isDisabled) {
itemStyle = textTheme.bodyMedium
?.apply(color: colorScheme.onSurface.withOpacity(0.38));
} else if (DateUtils.isSameDay(widget.currentDate, dayToBuild)) {
// The current day gets a different text color and a circle stroke
// border.
itemStyle = textTheme.bodyMedium?.apply(color: Color(0xff4D6FD5));
decoration = widget.currentDateDecoration ??
BoxDecoration(
border: Border.all(color: Color(0xff4D6FD5), width: 1),
shape: BoxShape.circle,
);
}
// We want the day of month to be spoken first irrespective of the
// locale-specific preferences or TextDirection. This is because
// an accessibility user is more likely to be interested in the
// day of month before the rest of the date, as they are looking
// for the day of month. To do that we prepend day of month to the
// formatted full date.
String semanticLabel =
'${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}';
if (isSelectedDayStart) {
semanticLabel =
localizations.dateRangeStartDateSemanticLabel(semanticLabel);
} else if (isSelectedDayEnd) {
semanticLabel =
localizations.dateRangeEndDateSemanticLabel(semanticLabel);
}
Widget dayWidget = Container(
decoration: decoration,
child: Center(
child: Semantics(
label: semanticLabel,
selected: isSelectedDayStart || isSelectedDayEnd,
child: ExcludeSemantics(
child: Text(localizations.formatDecimal(day), style: itemStyle),
),
),
),
);
if (highlightPainter != null) {
dayWidget = CustomPaint(
painter: highlightPainter,
child: dayWidget,
);
}
if (!isDisabled) {
dayWidget = InkResponse(
focusNode: _dayFocusNodes[day - 1],
onTap: () => widget.onChanged(dayToBuild),
radius: _monthItemRowHeight / 2 + 4,
splashColor: colorScheme.primary.withOpacity(0.38),
onFocusChange: _dayFocusChanged,
child: dayWidget,
);
}
return dayWidget;
}
Widget _buildEdgeContainer(BuildContext context, bool isHighlighted) {
return Container(color: isHighlighted ? _highlightColor(context) : null);
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final int year = widget.displayedMonth.year;
final int month = widget.displayedMonth.month;
final int daysInMonth = DateUtils.getDaysInMonth(year, month);
final int dayOffset = DateUtils.firstDayOffset(year, month, localizations);
final int weeks = ((daysInMonth + dayOffset) / DateTime.daysPerWeek).ceil();
final double gridHeight =
weeks * _monthItemRowHeight + (weeks - 1) * _monthItemSpaceBetweenRows;
final List<Widget> dayItems = <Widget>[];
for (int i = 0; true; i += 1) {
// 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
// a leap year.
final int day = i - dayOffset + 1;
if (day > daysInMonth) break;
if (day < 1) {
dayItems.add(Container());
} else {
final DateTime dayToBuild = DateTime(year, month, day);
final Widget dayItem = _buildDayItem(
context,
dayToBuild,
dayOffset,
daysInMonth,
);
dayItems.add(dayItem);
}
}
// Add the leading/trailing edge containers to each week in order to
// correctly extend the range highlight.
final List<Widget> paddedDayItems = <Widget>[];
for (int i = 0; i < weeks; i++) {
final int start = i * DateTime.daysPerWeek;
final int end = math.min(
start + DateTime.daysPerWeek,
dayItems.length,
);
final List<Widget> weekList = dayItems.sublist(start, end);
final DateTime dateAfterLeadingPadding =
DateTime(year, month, start - dayOffset + 1);
// Only color the edge container if it is after the start date and
// on/before the end date.
final bool isLeadingInRange = !(dayOffset > 0 && i == 0) &&
widget.selectedDateStart != null &&
widget.selectedDateEnd != null &&
dateAfterLeadingPadding.isAfter(widget.selectedDateStart!) &&
!dateAfterLeadingPadding.isAfter(widget.selectedDateEnd!);
weekList.insert(0, _buildEdgeContainer(context, isLeadingInRange));
// Only add a trailing edge container if it is for a full week and not a
// partial week.
if (end < dayItems.length ||
(end == dayItems.length &&
dayItems.length % DateTime.daysPerWeek == 0)) {
final DateTime dateBeforeTrailingPadding =
DateTime(year, month, end - dayOffset);
// Only color the edge container if it is on/after the start date and
// before the end date.
final bool isTrailingInRange = widget.selectedDateStart != null &&
widget.selectedDateEnd != null &&
!dateBeforeTrailingPadding.isBefore(widget.selectedDateStart!) &&
dateBeforeTrailingPadding.isBefore(widget.selectedDateEnd!);
weekList.add(_buildEdgeContainer(context, isTrailingInRange));
}
paddedDayItems.addAll(weekList);
}
final double maxWidth =
MediaQuery.of(context).orientation == Orientation.landscape
? _maxCalendarWidthLandscape
: _maxCalendarWidthPortrait;
return Column(
children: <Widget>[
Container(
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: gridHeight,
),
child: GridView.custom(
physics: const NeverScrollableScrollPhysics(),
gridDelegate: _monthItemGridDelegate,
padding: EdgeInsets.zero,
childrenDelegate: SliverChildListDelegate(
paddedDayItems,
addRepaintBoundaries: false,
),
),
),
const SizedBox(height: _monthItemFooterHeight),
],
);
}
}
///变量
const Duration _monthScrollDuration = Duration(milliseconds: 200);
const double _monthItemFooterHeight = 12.0;
const double _monthItemRowHeight = 42.0;
const double _monthItemSpaceBetweenRows = 8.0;
const double _horizontalPadding = 8.0;
const double _maxCalendarWidthLandscape = 384.0;
const double _maxCalendarWidthPortrait = 480.0;
class _FocusedDate extends InheritedWidget {
const _FocusedDate({
Key? key,
required Widget child,
this.date,
this.scrollDirection,
}) : super(key: key, child: child);
final DateTime? date;
final TraversalDirection? scrollDirection;
@override
bool updateShouldNotify(_FocusedDate oldWidget) {
return !DateUtils.isSameDay(date, oldWidget.date) ||
scrollDirection != oldWidget.scrollDirection;
}
static _FocusedDate? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_FocusedDate>();
}
}
///日期格式
class _MonthItemGridDelegate extends SliverGridDelegate {
const _MonthItemGridDelegate();
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
final double tileWidth =
(constraints.crossAxisExtent - 2 * _horizontalPadding) /
DateTime.daysPerWeek;
return _MonthSliverGridLayout(
crossAxisCount: DateTime.daysPerWeek + 2,
dayChildWidth: tileWidth,
edgeChildWidth: _horizontalPadding,
reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
);
}
@override
bool shouldRelayout(_MonthItemGridDelegate oldDelegate) => false;
}
const _MonthItemGridDelegate _monthItemGridDelegate = _MonthItemGridDelegate();
class _MonthSliverGridLayout extends SliverGridLayout {
/// Creates a layout that uses equally sized and spaced tiles for each day of
/// the week and an additional edge tile for padding at the start and end of
/// each row.
///
/// This is necessary to facilitate the painting of the range highlight
/// correctly.
const _MonthSliverGridLayout({
required this.crossAxisCount,
required this.dayChildWidth,
required this.edgeChildWidth,
required this.reverseCrossAxis,
}) : assert(crossAxisCount != null && crossAxisCount > 0),
assert(dayChildWidth != null && dayChildWidth >= 0),
assert(edgeChildWidth != null && edgeChildWidth >= 0),
assert(reverseCrossAxis != null);
/// The number of children in the cross axis.
final int crossAxisCount;
/// The width in logical pixels of the day child widgets.
final double dayChildWidth;
/// The width in logical pixels of the edge child widgets.
final double edgeChildWidth;
/// Whether the children should be placed in the opposite order of increasing
/// coordinates in the cross axis.
///
/// For example, if the cross axis is horizontal, the children are placed from
/// left to right when [reverseCrossAxis] is false and from right to left when
/// [reverseCrossAxis] is true.
///
/// Typically set to the return value of [axisDirectionIsReversed] applied to
/// the [SliverConstraints.crossAxisDirection].
final bool reverseCrossAxis;
/// The number of logical pixels from the leading edge of one row to the
/// leading edge of the next row.
double get _rowHeight {
return _monthItemRowHeight + _monthItemSpaceBetweenRows;
}
/// The height in logical pixels of the children widgets.
double get _childHeight {
return _monthItemRowHeight;
}
@override
int getMinChildIndexForScrollOffset(double scrollOffset) {
return crossAxisCount * (scrollOffset ~/ _rowHeight);
}
@override
int getMaxChildIndexForScrollOffset(double scrollOffset) {
final int mainAxisCount = (scrollOffset / _rowHeight).ceil();
return math.max(0, crossAxisCount * mainAxisCount - 1);
}
double _getCrossAxisOffset(double crossAxisStart, bool isPadding) {
if (reverseCrossAxis) {
return ((crossAxisCount - 2) * dayChildWidth + 2 * edgeChildWidth) -
crossAxisStart -
(isPadding ? edgeChildWidth : dayChildWidth);
}
return crossAxisStart;
}
@override
SliverGridGeometry getGeometryForChildIndex(int index) {
final int adjustedIndex = index % crossAxisCount;
final bool isEdge =
adjustedIndex == 0 || adjustedIndex == crossAxisCount - 1;
final double crossAxisStart =
math.max(0, (adjustedIndex - 1) * dayChildWidth + edgeChildWidth);
return SliverGridGeometry(
scrollOffset: (index ~/ crossAxisCount) * _rowHeight,
crossAxisOffset: _getCrossAxisOffset(crossAxisStart, isEdge),
mainAxisExtent: _childHeight,
crossAxisExtent: isEdge ? edgeChildWidth : dayChildWidth,
);
}
@override
double computeMaxScrollOffset(int childCount) {
assert(childCount >= 0);
final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1;
final double mainAxisSpacing = _rowHeight - _childHeight;
return _rowHeight * mainAxisCount - mainAxisSpacing;
}
}
///星期显示
class _DayHeaders extends StatelessWidget {
/// Builds widgets showing abbreviated days of week. The first widget in the
/// returned list corresponds to the first day of week for the current locale.
///
/// Examples:
///
/// ```
/// ┌ Sunday is the first day of week in the US (en_US)
/// |
/// S M T W T F S <-- the returned list contains these widgets
/// _ _ _ _ _ 1 2
/// 3 4 5 6 7 8 9
///
/// ┌ But it's Monday in the UK (en_GB)
/// |
/// M T W T F S S <-- the returned list contains these widgets
/// _ _ _ _ 1 2 3
/// 4 5 6 7 8 9 10
/// ```
_DayHeaders({this.weekTextStyle, this.weekendTextStyle});
///周六周天字体颜色
final TextStyle? weekendTextStyle;
///周字体颜色
final TextStyle? weekTextStyle;
List<Widget> _getDayHeaders(
TextStyle headerStyle, MaterialLocalizations localizations) {
final List<Widget> result = <Widget>[];
for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) {
final String weekday = localizations.narrowWeekdays[i];
result.add(ExcludeSemantics(
child: Center(
child: Text(weekday,
style: i == 0 || i == 6
? weekendTextStyle ?? weekTextStyle
: headerStyle)),
));
if (i == (localizations.firstDayOfWeekIndex - 1) % 7) break;
}
return result;
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final ColorScheme colorScheme = themeData.colorScheme;
final TextStyle textStyle = weekTextStyle ??
themeData.textTheme.titleMedium!.apply(color: colorScheme.onSurface);
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final List<Widget> labels = _getDayHeaders(textStyle, localizations);
// Add leading and trailing containers for edges of the custom grid layout.
labels.insert(0, Container());
labels.add(Container());
return Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).orientation == Orientation.landscape
? _maxCalendarWidthLandscape
: _maxCalendarWidthPortrait,
maxHeight: _monthItemRowHeight,
),
child: GridView.custom(
shrinkWrap: true,
gridDelegate: _monthItemGridDelegate,
childrenDelegate: SliverChildListDelegate(
labels,
addRepaintBoundaries: false,
),
),
);
}
}
///日期滚动控制组件
class _CalendarKeyboardNavigator extends StatefulWidget {
const _CalendarKeyboardNavigator({
Key? key,
required this.child,
required this.firstDate,
required this.lastDate,
required this.initialFocusedDay,
}) : super(key: key);
final Widget child;
final DateTime firstDate;
final DateTime lastDate;
final DateTime initialFocusedDay;
@override
_CalendarKeyboardNavigatorState createState() =>
_CalendarKeyboardNavigatorState();
}
class _CalendarKeyboardNavigatorState
extends State<_CalendarKeyboardNavigator> {
late Map<LogicalKeySet, Intent> _shortcutMap;
late Map<Type, Action<Intent>> _actionMap;
late FocusNode _dayGridFocus;
TraversalDirection? _dayTraversalDirection;
DateTime? _focusedDay;
@override
void initState() {
super.initState();
_shortcutMap = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.arrowLeft):
const DirectionalFocusIntent(TraversalDirection.left),
LogicalKeySet(LogicalKeyboardKey.arrowRight):
const DirectionalFocusIntent(TraversalDirection.right),
LogicalKeySet(LogicalKeyboardKey.arrowDown):
const DirectionalFocusIntent(TraversalDirection.down),
LogicalKeySet(LogicalKeyboardKey.arrowUp):
const DirectionalFocusIntent(TraversalDirection.up),
};
_actionMap = <Type, Action<Intent>>{
NextFocusIntent:
CallbackAction<NextFocusIntent>(onInvoke: _handleGridNextFocus),
PreviousFocusIntent: CallbackAction<PreviousFocusIntent>(
onInvoke: _handleGridPreviousFocus),
DirectionalFocusIntent: CallbackAction<DirectionalFocusIntent>(
onInvoke: _handleDirectionFocus),
};
_dayGridFocus = FocusNode(debugLabel: 'Day Grid');
}
@override
void dispose() {
_dayGridFocus.dispose();
super.dispose();
}
void _handleGridFocusChange(bool focused) {
setState(() {
if (focused) {
_focusedDay ??= widget.initialFocusedDay;
}
});
}
/// Move focus to the next element after the day grid.
void _handleGridNextFocus(NextFocusIntent intent) {
_dayGridFocus.requestFocus();
_dayGridFocus.nextFocus();
}
/// Move focus to the previous element before the day grid.
void _handleGridPreviousFocus(PreviousFocusIntent intent) {
_dayGridFocus.requestFocus();
_dayGridFocus.previousFocus();
}
/// Move the internal focus date in the direction of the given intent.
///
/// This will attempt to move the focused day to the next selectable day in
/// the given direction. If the new date is not in the current month, then
/// the page view will be scrolled to show the new date's month.
///
/// For horizontal directions, it will move forward or backward a day (depending
/// on the current [TextDirection]). For vertical directions it will move up and
/// down a week at a time.
void _handleDirectionFocus(DirectionalFocusIntent intent) {
assert(_focusedDay != null);
setState(() {
final DateTime? nextDate =
_nextDateInDirection(_focusedDay!, intent.direction);
if (nextDate != null) {
_focusedDay = nextDate;
_dayTraversalDirection = intent.direction;
}
});
}
static const Map<TraversalDirection, int> _directionOffset =
<TraversalDirection, int>{
TraversalDirection.up: -DateTime.daysPerWeek,
TraversalDirection.right: 1,
TraversalDirection.down: DateTime.daysPerWeek,
TraversalDirection.left: -1,
};
int _dayDirectionOffset(
TraversalDirection traversalDirection, TextDirection textDirection) {
// Swap left and right if the text direction if RTL
if (textDirection == TextDirection.rtl) {
if (traversalDirection == TraversalDirection.left)
traversalDirection = TraversalDirection.right;
else if (traversalDirection == TraversalDirection.right)
traversalDirection = TraversalDirection.left;
}
return _directionOffset[traversalDirection]!;
}
DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
final TextDirection textDirection = Directionality.of(context);
final DateTime nextDate = DateUtils.addDaysToDate(
date, _dayDirectionOffset(direction, textDirection));
if (!nextDate.isBefore(widget.firstDate) &&
!nextDate.isAfter(widget.lastDate)) {
return nextDate;
}
return null;
}
@override
Widget build(BuildContext context) {
return FocusableActionDetector(
shortcuts: _shortcutMap,
actions: _actionMap,
focusNode: _dayGridFocus,
onFocusChange: _handleGridFocusChange,
child: _FocusedDate(
date: _dayGridFocus.hasFocus ? _focusedDay : null,
scrollDirection: _dayGridFocus.hasFocus ? _dayTraversalDirection : null,
child: widget.child,
),
);
}
}
///高度绘制
class _HighlightPainter extends CustomPainter {
_HighlightPainter({
required this.color,
this.style = _HighlightPainterStyle.none,
this.textDirection,
});
final Color color;
final _HighlightPainterStyle style;
final TextDirection? textDirection;
@override
void paint(Canvas canvas, Size size) {
if (style == _HighlightPainterStyle.none) {
return;
}
final Paint paint = Paint()
..color = color
..style = PaintingStyle.fill;
final Rect rectLeft = Rect.fromLTWH(0, 0, size.width / 2, size.height);
final Rect rectRight =
Rect.fromLTWH(size.width / 2, 0, size.width / 2, size.height);
switch (style) {
case _HighlightPainterStyle.highlightTrailing:
canvas.drawRect(
textDirection == TextDirection.ltr ? rectRight : rectLeft,
paint,
);
break;
case _HighlightPainterStyle.highlightLeading:
canvas.drawRect(
textDirection == TextDirection.ltr ? rectLeft : rectRight,
paint,
);
break;
case _HighlightPainterStyle.highlightAll:
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
paint,
);
break;
case _HighlightPainterStyle.none:
break;
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
enum _HighlightPainterStyle {
/// Paints nothing.
none,
/// Paints a rectangle that occupies the leading half of the space.
highlightLeading,
/// Paints a rectangle that occupies the trailing half of the space.
highlightTrailing,
/// Paints a rectangle that occupies all available space.
highlightAll,
}