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 CustomCalendarRangePickerWidget extends StatefulWidget { CustomCalendarRangePickerWidget( {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 _CustomCalendarRangePickerWidgetState createState() => _CustomCalendarRangePickerWidgetState(); } class _CustomCalendarRangePickerWidgetState extends State { 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: [ _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: [ 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 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 _dayFocusNodes; @override void initState() { super.initState(); final int daysInMonth = DateUtils.getDaysInMonth( widget.displayedMonth.year, widget.displayedMonth.month); _dayFocusNodes = List.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: const Color(0xff4D6FD5)); decoration = widget.currentDateDecoration ?? BoxDecoration( border: Border.all(color: const 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 dayItems = []; 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 paddedDayItems = []; 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 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: [ 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 _getDayHeaders( TextStyle headerStyle, MaterialLocalizations localizations) { final List result = []; 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.bodyMedium!.apply(color: colorScheme.onSurface); final MaterialLocalizations localizations = MaterialLocalizations.of(context); final List 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 _shortcutMap; late Map> _actionMap; late FocusNode _dayGridFocus; TraversalDirection? _dayTraversalDirection; DateTime? _focusedDay; @override void initState() { super.initState(); _shortcutMap = { 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 = >{ NextFocusIntent: CallbackAction(onInvoke: _handleGridNextFocus), PreviousFocusIntent: CallbackAction( onInvoke: _handleGridPreviousFocus), DirectionalFocusIntent: CallbackAction( 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 _directionOffset = { 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, }