diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index 303bb43be1..9bc419eac5 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -868,10 +868,6 @@ class _DayPickerState extends State<_DayPicker> { /// List of [FocusNode]s, one for each day of the month. late List _dayFocusNodes; - // TODO(polina-c): a cleaner solution is to create separate statefull widget for a day. - // https://github.com/flutter/flutter/issues/134323 - final Map _statesControllers = {}; - @override void initState() { super.initState(); @@ -897,9 +893,6 @@ class _DayPickerState extends State<_DayPicker> { for (final FocusNode node in _dayFocusNodes) { node.dispose(); } - for (final MaterialStatesController controller in _statesControllers.values) { - controller.dispose(); - } super.dispose(); } @@ -937,7 +930,6 @@ class _DayPickerState extends State<_DayPicker> { final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context); final DatePickerThemeData defaults = DatePickerTheme.defaults(context); final TextStyle? weekdayStyle = datePickerTheme.weekdayStyle ?? defaults.weekdayStyle; - final TextStyle? dayStyle = datePickerTheme.dayStyle ?? defaults.dayStyle; final int year = widget.displayedMonth.year; final int month = widget.displayedMonth.month; @@ -945,18 +937,6 @@ class _DayPickerState extends State<_DayPicker> { final int daysInMonth = DateUtils.getDaysInMonth(year, month); final int dayOffset = DateUtils.firstDayOffset(year, month, localizations); - T? effectiveValue(T? Function(DatePickerThemeData? theme) getProperty) { - return getProperty(datePickerTheme) ?? getProperty(defaults); - } - - T? resolve(MaterialStateProperty? Function(DatePickerThemeData? theme) getProperty, Set states) { - return effectiveValue( - (DatePickerThemeData? theme) { - return getProperty(theme)?.resolve(states); - }, - ); - } - final List dayItems = _dayHeaders(weekdayStyle, localizations); // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on // a leap year. @@ -973,71 +953,18 @@ class _DayPickerState extends State<_DayPicker> { (widget.selectableDayPredicate != null && !widget.selectableDayPredicate!(dayToBuild)); final bool isSelectedDay = DateUtils.isSameDay(widget.selectedDate, dayToBuild); final bool isToday = DateUtils.isSameDay(widget.currentDate, dayToBuild); - final String semanticLabelSuffix = isToday ? ', ${localizations.currentDateLabel}' : ''; - final Set states = { - if (isDisabled) MaterialState.disabled, - if (isSelectedDay) MaterialState.selected, - }; - - final MaterialStatesController statesController = _statesControllers.putIfAbsent(day, () => MaterialStatesController()); - statesController.value = states; - - final Color? dayForegroundColor = resolve((DatePickerThemeData? theme) => isToday ? theme?.todayForegroundColor : theme?.dayForegroundColor, states); - final Color? dayBackgroundColor = resolve((DatePickerThemeData? theme) => isToday ? theme?.todayBackgroundColor : theme?.dayBackgroundColor, states); - final MaterialStateProperty dayOverlayColor = MaterialStateProperty.resolveWith( - (Set states) => effectiveValue((DatePickerThemeData? theme) => theme?.dayOverlayColor?.resolve(states)), - ); - final BoxDecoration decoration = isToday - ? BoxDecoration( - color: dayBackgroundColor, - border: Border.fromBorderSide( - (datePickerTheme.todayBorder ?? defaults.todayBorder!) - .copyWith(color: dayForegroundColor) - ), - shape: BoxShape.circle, - ) - : BoxDecoration( - color: dayBackgroundColor, - shape: BoxShape.circle, - ); - - Widget dayWidget = Container( - decoration: decoration, - child: Center( - child: Text(localizations.formatDecimal(day), style: dayStyle?.apply(color: dayForegroundColor)), + dayItems.add( + _Day( + dayToBuild, + key: ValueKey(dayToBuild), + isDisabled: isDisabled, + isSelectedDay: isSelectedDay, + isToday: isToday, + onChanged: widget.onChanged, + focusNode: _dayFocusNodes[day - 1], ), ); - - if (isDisabled) { - dayWidget = ExcludeSemantics( - child: dayWidget, - ); - } else { - dayWidget = InkResponse( - focusNode: _dayFocusNodes[day - 1], - onTap: () => widget.onChanged(dayToBuild), - radius: _dayPickerRowHeight / 2 + 4, - statesController: statesController, - overlayColor: dayOverlayColor, - child: Semantics( - // 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. - label: '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}$semanticLabelSuffix', - // Set button to true to make the date selectable. - button: true, - selected: isSelectedDay, - excludeSemantics: true, - child: dayWidget, - ), - ); - } - - dayItems.add(dayWidget); } } @@ -1057,6 +984,122 @@ class _DayPickerState extends State<_DayPicker> { } } +class _Day extends StatefulWidget { + const _Day( + this.day, { + super.key, + required this.isDisabled, + required this.isSelectedDay, + required this.isToday, + required this.onChanged, + required this.focusNode, + }); + + final DateTime day; + final bool isDisabled; + final bool isSelectedDay; + final bool isToday; + final ValueChanged onChanged; + final FocusNode? focusNode; + + @override + State<_Day> createState() => _DayState(); +} + +class _DayState extends State<_Day> { + final MaterialStatesController _statesController = MaterialStatesController(); + + @override + Widget build(BuildContext context) { + final DatePickerThemeData defaults = DatePickerTheme.defaults(context); + final DatePickerThemeData datePickerTheme = DatePickerTheme.of(context); + final TextStyle? dayStyle = datePickerTheme.dayStyle ?? defaults.dayStyle; + T? effectiveValue(T? Function(DatePickerThemeData? theme) getProperty) { + return getProperty(datePickerTheme) ?? getProperty(defaults); + } + + T? resolve(MaterialStateProperty? Function(DatePickerThemeData? theme) getProperty, Set states) { + return effectiveValue( + (DatePickerThemeData? theme) { + return getProperty(theme)?.resolve(states); + }, + ); + } + + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final String semanticLabelSuffix = widget.isToday ? ', ${localizations.currentDateLabel}' : ''; + + final Set states = { + if (widget.isDisabled) MaterialState.disabled, + if (widget.isSelectedDay) MaterialState.selected, + }; + + _statesController.value = states; + + final Color? dayForegroundColor = resolve((DatePickerThemeData? theme) => widget.isToday ? theme?.todayForegroundColor : theme?.dayForegroundColor, states); + final Color? dayBackgroundColor = resolve((DatePickerThemeData? theme) => widget.isToday ? theme?.todayBackgroundColor : theme?.dayBackgroundColor, states); + final MaterialStateProperty dayOverlayColor = MaterialStateProperty.resolveWith( + (Set states) => effectiveValue((DatePickerThemeData? theme) => theme?.dayOverlayColor?.resolve(states)), + ); + final BoxDecoration decoration = widget.isToday + ? BoxDecoration( + color: dayBackgroundColor, + border: Border.fromBorderSide( + (datePickerTheme.todayBorder ?? defaults.todayBorder!) + .copyWith(color: dayForegroundColor) + ), + shape: BoxShape.circle, + ) + : BoxDecoration( + color: dayBackgroundColor, + shape: BoxShape.circle, + ); + + Widget dayWidget = Container( + decoration: decoration, + child: Center( + child: Text(localizations.formatDecimal(widget.day.day), style: dayStyle?.apply(color: dayForegroundColor)), + ), + ); + + if (widget.isDisabled) { + dayWidget = ExcludeSemantics( + child: dayWidget, + ); + } else { + dayWidget = InkResponse( + focusNode: widget.focusNode, + onTap: () => widget.onChanged(widget.day), + radius: _dayPickerRowHeight / 2 + 4, + statesController: _statesController, + overlayColor: dayOverlayColor, + child: Semantics( + // 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. + label: '${localizations.formatDecimal(widget.day.day)}, ${localizations.formatFullDate(widget.day)}$semanticLabelSuffix', + // Set button to true to make the date selectable. + button: true, + selected: widget.isSelectedDay, + excludeSemantics: true, + child: dayWidget, + ), + ); + } + + return dayWidget; + } + + @override + void dispose() { + _statesController.dispose(); + super.dispose(); + } +} + class _DayPickerGridDelegate extends SliverGridDelegate { const _DayPickerGridDelegate();