diff --git a/dev/tools/gen_defaults/lib/date_picker_template.dart b/dev/tools/gen_defaults/lib/date_picker_template.dart index e1c8836dee8..6b803b85d75 100644 --- a/dev/tools/gen_defaults/lib/date_picker_template.dart +++ b/dev/tools/gen_defaults/lib/date_picker_template.dart @@ -44,6 +44,9 @@ class _${blockName}DefaultsM3 extends DatePickerThemeData { : super( elevation: ${elevation("md.comp.date-picker.modal.container")}, shape: ${shape("md.comp.date-picker.modal.container")}, + // TODO(tahatesser): Update this to use token when gen_defaults + // supports `CircleBorder` for fully rounded corners. + dayShape: const MaterialStatePropertyAll(CircleBorder()), rangePickerElevation: ${elevation("md.comp.date-picker.modal.range-selection.container")}, rangePickerShape: ${shape("md.comp.date-picker.modal.range-selection.container")}, ); diff --git a/examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart b/examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart new file mode 100644 index 00000000000..41cd9812df2 --- /dev/null +++ b/examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart @@ -0,0 +1,64 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for [DatePickerThemeData]. + +void main() => runApp(const DatePickerApp()); + +class DatePickerApp extends StatelessWidget { + const DatePickerApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + datePickerTheme: DatePickerThemeData( + todayBackgroundColor: const MaterialStatePropertyAll(Colors.amber), + todayForegroundColor: const MaterialStatePropertyAll(Colors.black), + todayBorder: const BorderSide(width: 2), + dayShape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + ), + ), + home: const DatePickerExample(), + ); + } +} + +class DatePickerExample extends StatefulWidget { + const DatePickerExample({super.key}); + + @override + State createState() => _DatePickerExampleState(); +} + +class _DatePickerExampleState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: OutlinedButton( + onPressed: () { + showDatePicker( + context: context, + initialDate: DateTime(2021, 1, 20), + currentDate: DateTime(2021, 1, 15), + firstDate: DateTime(2021), + lastDate: DateTime(2022), + ); + }, + child: const Text('Open Date Picker'), + ), + ), + ); + } +} diff --git a/examples/api/test/material/date_picker/date_picker_theme_day_shape.0_test.dart b/examples/api/test/material/date_picker/date_picker_theme_day_shape.0_test.dart new file mode 100644 index 00000000000..dc239d90494 --- /dev/null +++ b/examples/api/test/material/date_picker/date_picker_theme_day_shape.0_test.dart @@ -0,0 +1,55 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/date_picker/date_picker_theme_day_shape.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('DatePickerThemeData.dayShape updates day selection shape decoration', (WidgetTester tester) async { + final ThemeData theme = ThemeData(); + final OutlinedBorder dayShape = RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)); + const Color todayBackgroundColor = Colors.amber; + const Color todayForegroundColor = Colors.black; + const BorderSide todayBorder = BorderSide(width: 2); + + await tester.pumpWidget( + const example.DatePickerApp(), + ); + + await tester.tap(find.text('Open Date Picker')); + await tester.pumpAndSettle(); + + ShapeDecoration dayShapeDecoration = tester.widget(find.ancestor( + of: find.text('15'), + matching: find.byType(DecoratedBox), + )).decoration as ShapeDecoration; + + // Test the current day shape decoration. + expect(dayShapeDecoration.color, todayBackgroundColor); + expect(dayShapeDecoration.shape, dayShape.copyWith(side: todayBorder.copyWith(color: todayForegroundColor))); + + dayShapeDecoration = tester.widget(find.ancestor( + of: find.text('20'), + matching: find.byType(DecoratedBox), + )).decoration as ShapeDecoration; + + // Test the selected day shape decoration. + expect(dayShapeDecoration.color, theme.colorScheme.primary); + expect(dayShapeDecoration.shape, dayShape); + + // Tap to select current day as the selected day. + await tester.tap(find.text('15')); + await tester.pumpAndSettle(); + + dayShapeDecoration = tester.widget(find.ancestor( + of: find.text('15'), + matching: find.byType(DecoratedBox), + )).decoration as ShapeDecoration; + + // Test the selected day shape decoration. + expect(dayShapeDecoration.color, todayBackgroundColor); + expect(dayShapeDecoration.shape, dayShape.copyWith(side: todayBorder.copyWith(color: todayForegroundColor))); + }); +} diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index d203a96cadd..aa25b7e1f86 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -1057,21 +1057,21 @@ class _DayState extends State<_Day> { final MaterialStateProperty dayOverlayColor = MaterialStateProperty.resolveWith( (Set states) => effectiveValue((DatePickerThemeData? theme) => theme?.dayOverlayColor?.resolve(states)), ); - final BoxDecoration decoration = widget.isToday - ? BoxDecoration( + final OutlinedBorder dayShape = resolve((DatePickerThemeData? theme) => theme?.dayShape, states)!; + final ShapeDecoration decoration = widget.isToday + ? ShapeDecoration( color: dayBackgroundColor, - border: Border.fromBorderSide( - (datePickerTheme.todayBorder ?? defaults.todayBorder!) - .copyWith(color: dayForegroundColor) + shape: dayShape.copyWith( + side: (datePickerTheme.todayBorder ?? defaults.todayBorder!) + .copyWith(color: dayForegroundColor), ), - shape: BoxShape.circle, ) - : BoxDecoration( + : ShapeDecoration( color: dayBackgroundColor, - shape: BoxShape.circle, + shape: dayShape, ); - Widget dayWidget = Container( + Widget dayWidget = DecoratedBox( decoration: decoration, child: Center( child: Text(localizations.formatDecimal(widget.day.day), style: dayStyle?.apply(color: dayForegroundColor)), @@ -1086,9 +1086,10 @@ class _DayState extends State<_Day> { dayWidget = InkResponse( focusNode: widget.focusNode, onTap: () => widget.onChanged(widget.day), - radius: _dayPickerRowHeight / 2 + 4, statesController: _statesController, overlayColor: dayOverlayColor, + customBorder: dayShape, + containedInkWell: true, child: Semantics( // We want the day of month to be spoken first irrespective of the // locale-specific preferences or TextDirection. This is because diff --git a/packages/flutter/lib/src/material/date_picker_theme.dart b/packages/flutter/lib/src/material/date_picker_theme.dart index cd81c2eee0a..60c46c4554b 100644 --- a/packages/flutter/lib/src/material/date_picker_theme.dart +++ b/packages/flutter/lib/src/material/date_picker_theme.dart @@ -52,6 +52,7 @@ class DatePickerThemeData with Diagnosticable { this.dayForegroundColor, this.dayBackgroundColor, this.dayOverlayColor, + this.dayShape, this.todayForegroundColor, this.todayBackgroundColor, this.todayBorder, @@ -163,12 +164,41 @@ class DatePickerThemeData with Diagnosticable { /// indicate that a day in the grid is focused, hovered, or pressed. final MaterialStateProperty? dayOverlayColor; + /// Overrides the default shape used to paint the shape decoration of the + /// day labels in the grid of the date picker. + /// + /// If the selected day is the current day, the provided shape with the + /// value of [todayBackgroundColor] is used to paint the shape decoration of + /// the day label and the value of [todayBorder] and [todayForegroundColor] is + /// used to paint the border. + /// + /// If the selected day is not the current day, the provided shape with the + /// value of [dayBackgroundColor] is used to paint the shape decoration of + /// the day label. + /// + /// {@tool dartpad} + /// This sample demonstrates how to customize the day selector shape decoration + /// using the [dayShape], [todayForegroundColor], [todayBackgroundColor], and + /// [todayBorder] properties. + /// + /// ** See code in examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart ** + /// {@end-tool} + final MaterialStateProperty? dayShape; + /// Overrides the default color used to paint the /// [DatePickerDialog.currentDate] label in the grid of the dialog's /// [CalendarDatePicker] and the corresponding year in the dialog's /// [YearPicker]. /// /// This will be used instead of the [TextStyle.color] provided in [dayStyle]. + /// + /// {@tool dartpad} + /// This sample demonstrates how to customize the day selector shape decoration + /// using the [dayShape], [todayForegroundColor], [todayBackgroundColor], and + /// [todayBorder] properties. + /// + /// ** See code in examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart ** + /// {@end-tool} final MaterialStateProperty? todayForegroundColor; /// Overrides the default color used to paint the background of the @@ -181,6 +211,14 @@ class DatePickerThemeData with Diagnosticable { /// /// The border side's [BorderSide.color] is not used, /// [todayForegroundColor] is used instead. + /// + /// {@tool dartpad} + /// This sample demonstrates how to customize the day selector shape decoration + /// using the [dayShape], [todayForegroundColor], [todayBackgroundColor], and + /// [todayBorder] properties. + /// + /// ** See code in examples/api/lib/material/date_picker/date_picker_theme_day_shape.0.dart ** + /// {@end-tool} final BorderSide? todayBorder; /// Overrides the default text style used to paint each of the year @@ -326,6 +364,7 @@ class DatePickerThemeData with Diagnosticable { MaterialStateProperty? dayForegroundColor, MaterialStateProperty? dayBackgroundColor, MaterialStateProperty? dayOverlayColor, + MaterialStateProperty? dayShape, MaterialStateProperty? todayForegroundColor, MaterialStateProperty? todayBackgroundColor, BorderSide? todayBorder, @@ -364,6 +403,7 @@ class DatePickerThemeData with Diagnosticable { dayForegroundColor: dayForegroundColor ?? this.dayForegroundColor, dayBackgroundColor: dayBackgroundColor ?? this.dayBackgroundColor, dayOverlayColor: dayOverlayColor ?? this.dayOverlayColor, + dayShape: dayShape ?? this.dayShape, todayForegroundColor: todayForegroundColor ?? this.todayForegroundColor, todayBackgroundColor: todayBackgroundColor ?? this.todayBackgroundColor, todayBorder: todayBorder ?? this.todayBorder, @@ -409,6 +449,7 @@ class DatePickerThemeData with Diagnosticable { dayForegroundColor: MaterialStateProperty.lerp(a?.dayForegroundColor, b?.dayForegroundColor, t, Color.lerp), dayBackgroundColor: MaterialStateProperty.lerp(a?.dayBackgroundColor, b?.dayBackgroundColor, t, Color.lerp), dayOverlayColor: MaterialStateProperty.lerp(a?.dayOverlayColor, b?.dayOverlayColor, t, Color.lerp), + dayShape: MaterialStateProperty.lerp(a?.dayShape, b?.dayShape, t, OutlinedBorder.lerp), todayForegroundColor: MaterialStateProperty.lerp(a?.todayForegroundColor, b?.todayForegroundColor, t, Color.lerp), todayBackgroundColor: MaterialStateProperty.lerp(a?.todayBackgroundColor, b?.todayBackgroundColor, t, Color.lerp), todayBorder: _lerpBorderSide(a?.todayBorder, b?.todayBorder, t), @@ -460,6 +501,7 @@ class DatePickerThemeData with Diagnosticable { dayForegroundColor, dayBackgroundColor, dayOverlayColor, + dayShape, todayForegroundColor, todayBackgroundColor, todayBorder, @@ -504,6 +546,7 @@ class DatePickerThemeData with Diagnosticable { && other.dayForegroundColor == dayForegroundColor && other.dayBackgroundColor == dayBackgroundColor && other.dayOverlayColor == dayOverlayColor + && other.dayShape == dayShape && other.todayForegroundColor == todayForegroundColor && other.todayBackgroundColor == todayBackgroundColor && other.todayBorder == todayBorder @@ -545,6 +588,7 @@ class DatePickerThemeData with Diagnosticable { properties.add(DiagnosticsProperty>('dayForegroundColor', dayForegroundColor, defaultValue: null)); properties.add(DiagnosticsProperty>('dayBackgroundColor', dayBackgroundColor, defaultValue: null)); properties.add(DiagnosticsProperty>('dayOverlayColor', dayOverlayColor, defaultValue: null)); + properties.add(DiagnosticsProperty>('dayShape', dayShape, defaultValue: null)); properties.add(DiagnosticsProperty>('todayForegroundColor', todayForegroundColor, defaultValue: null)); properties.add(DiagnosticsProperty>('todayBackgroundColor', todayBackgroundColor, defaultValue: null)); properties.add(DiagnosticsProperty('todayBorder', todayBorder, defaultValue: null)); @@ -672,6 +716,7 @@ class _DatePickerDefaultsM2 extends DatePickerThemeData { : super( elevation: 24.0, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))), + dayShape: const MaterialStatePropertyAll(CircleBorder()), rangePickerElevation: 0.0, rangePickerShape: const RoundedRectangleBorder(), ); @@ -843,6 +888,9 @@ class _DatePickerDefaultsM3 extends DatePickerThemeData { : super( elevation: 6.0, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0))), + // TODO(tahatesser): Update this to use token when gen_defaults + // supports `CircleBorder` for fully rounded corners. + dayShape: const MaterialStatePropertyAll(CircleBorder()), rangePickerElevation: 0.0, rangePickerShape: const RoundedRectangleBorder(), ); diff --git a/packages/flutter/test/material/calendar_date_picker_test.dart b/packages/flutter/test/material/calendar_date_picker_test.dart index 589104df7dc..ff76c42b8ad 100644 --- a/packages/flutter/test/material/calendar_date_picker_test.dart +++ b/packages/flutter/test/material/calendar_date_picker_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -25,10 +26,11 @@ void main() { DatePickerMode initialCalendarMode = DatePickerMode.day, SelectableDayPredicate? selectableDayPredicate, TextDirection textDirection = TextDirection.ltr, + ThemeData? theme, bool? useMaterial3, }) { return MaterialApp( - theme: ThemeData(useMaterial3: useMaterial3), + theme: theme ?? ThemeData(useMaterial3: useMaterial3), home: Material( child: Directionality( textDirection: textDirection, @@ -1132,6 +1134,43 @@ void main() { semantics.dispose(); }, variant: TargetPlatformVariant.desktop()); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/141350. + testWidgets('Default day selection overlay', (WidgetTester tester) async { + final ThemeData theme = ThemeData(); + await tester.pumpWidget(calendarDatePicker( + firstDate: DateTime(2016, DateTime.december, 15), + initialDate: DateTime(2017, DateTime.january, 15), + lastDate: DateTime(2017, DateTime.february, 15), + onDisplayedMonthChanged: (DateTime date) {}, + theme: theme, + )); + + RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + expect(inkFeatures, isNot(paints..circle(radius: 35.0, color: theme.colorScheme.onSurfaceVariant.withOpacity(0.08)))); + expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 0)); + + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.text('25'))); + await tester.pumpAndSettle(); + inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); + expect(inkFeatures, paints..circle(radius: 35.0, color: theme.colorScheme.onSurfaceVariant.withOpacity(0.08))); + expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 1)); + + final Rect expectedClipRect = Rect.fromCircle(center: const Offset(400.0, 241.0), radius: 35.0); + final Path expectedClipPath = Path()..addRect(expectedClipRect); + expect( + inkFeatures, + paints..clipPath(pathMatcher: coversSameAreaAs( + expectedClipPath, + areaToCompare: expectedClipRect, + sampleSize: 100, + )), + ); + }); }); group('YearPicker', () { diff --git a/packages/flutter/test/material/date_picker_theme_test.dart b/packages/flutter/test/material/date_picker_theme_test.dart index 571af49df84..0a135d5c51b 100644 --- a/packages/flutter/test/material/date_picker_theme_test.dart +++ b/packages/flutter/test/material/date_picker_theme_test.dart @@ -24,6 +24,7 @@ void main() { dayForegroundColor: MaterialStatePropertyAll(Color(0xfffffff5)), dayBackgroundColor: MaterialStatePropertyAll(Color(0xfffffff6)), dayOverlayColor: MaterialStatePropertyAll(Color(0xfffffff7)), + dayShape: MaterialStatePropertyAll(RoundedRectangleBorder()), todayForegroundColor: MaterialStatePropertyAll(Color(0xfffffff8)), todayBackgroundColor: MaterialStatePropertyAll(Color(0xfffffff9)), todayBorder: BorderSide(width: 3), @@ -79,6 +80,15 @@ void main() { return container.decoration as BoxDecoration?; } + ShapeDecoration? findDayDecoration(WidgetTester tester, String day) { + return tester.widget( + find.ancestor( + of: find.text(day), + matching: find.byType(DecoratedBox) + ), + ).decoration as ShapeDecoration?; + } + ButtonStyle actionButtonStyle(WidgetTester tester, String text) { return tester.widget(find.widgetWithText(TextButton, text)).style!; } @@ -112,6 +122,7 @@ void main() { expect(theme.dayForegroundColor, null); expect(theme.dayBackgroundColor, null); expect(theme.dayOverlayColor, null); + expect(theme.dayShape, null); expect(theme.todayForegroundColor, null); expect(theme.todayBackgroundColor, null); expect(theme.todayBorder, null); @@ -183,6 +194,7 @@ void main() { expect(m3.dayOverlayColor?.resolve({MaterialState.selected, MaterialState.hovered, MaterialState.pressed}), colorScheme.onPrimary.withOpacity(0.1)); expect(m3.dayOverlayColor?.resolve({MaterialState.hovered, MaterialState.focused}), colorScheme.onSurfaceVariant.withOpacity(0.08)); expect(m3.dayOverlayColor?.resolve({MaterialState.hovered, MaterialState.pressed}), colorScheme.onSurfaceVariant.withOpacity(0.1)); + expect(m3.dayShape?.resolve({}), const CircleBorder()); expect(m3.todayForegroundColor?.resolve({}), colorScheme.primary); expect(m3.todayForegroundColor?.resolve({MaterialState.disabled}), colorScheme.primary.withOpacity(0.38)); expect(m3.todayBorder, BorderSide(color: colorScheme.primary)); @@ -256,6 +268,7 @@ void main() { expect(m2.dayOverlayColor?.resolve({MaterialState.hovered}), colorScheme.onSurfaceVariant.withOpacity(0.08)); expect(m2.dayOverlayColor?.resolve({MaterialState.focused}), colorScheme.onSurfaceVariant.withOpacity(0.12)); expect(m2.dayOverlayColor?.resolve({MaterialState.pressed}), colorScheme.onSurfaceVariant.withOpacity(0.12)); + expect(m2.dayShape?.resolve({}), const CircleBorder()); expect(m2.todayForegroundColor?.resolve({}), colorScheme.primary); expect(m2.todayForegroundColor?.resolve({MaterialState.disabled}), colorScheme.onSurface.withOpacity(0.38)); expect(m2.todayBorder, BorderSide(color: colorScheme.primary)); @@ -319,6 +332,7 @@ void main() { 'dayForegroundColor: MaterialStatePropertyAll(Color(0xfffffff5))', 'dayBackgroundColor: MaterialStatePropertyAll(Color(0xfffffff6))', 'dayOverlayColor: MaterialStatePropertyAll(Color(0xfffffff7))', + 'dayShape: MaterialStatePropertyAll(RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero))', 'todayForegroundColor: MaterialStatePropertyAll(Color(0xfffffff8))', 'todayBackgroundColor: MaterialStatePropertyAll(Color(0xfffffff9))', 'todayBorder: BorderSide(width: 3.0)', @@ -389,18 +403,24 @@ void main() { expect(selectedDate.style?.fontSize, datePickerTheme.headerHeadlineStyle?.fontSize); final Text day31 = tester.widget(find.text('31')); - final BoxDecoration day31Decoration = findTextDecoration(tester, '31')!; + final ShapeDecoration day31Decoration = findDayDecoration(tester, '31')!; expect(day31.style?.color, datePickerTheme.dayForegroundColor?.resolve({})); expect(day31.style?.fontSize, datePickerTheme.dayStyle?.fontSize); expect(day31Decoration.color, datePickerTheme.dayBackgroundColor?.resolve({})); + expect(day31Decoration.shape, datePickerTheme.dayShape?.resolve({})); final Text day24 = tester.widget(find.text('24')); // DatePickerDialog.currentDate - final BoxDecoration day24Decoration = findTextDecoration(tester, '24')!; + final ShapeDecoration day24Decoration = findDayDecoration(tester, '24')!; + final OutlinedBorder day24Shape = day24Decoration.shape as OutlinedBorder; expect(day24.style?.fontSize, datePickerTheme.dayStyle?.fontSize); expect(day24.style?.color, datePickerTheme.todayForegroundColor?.resolve({})); expect(day24Decoration.color, datePickerTheme.todayBackgroundColor?.resolve({})); - expect(day24Decoration.border?.top.width, datePickerTheme.todayBorder?.width); - expect(day24Decoration.border?.bottom.width, datePickerTheme.todayBorder?.width); + expect( + day24Decoration.shape, + datePickerTheme.dayShape?.resolve({})! + .copyWith(side: datePickerTheme.todayBorder?.copyWith(color: datePickerTheme.todayForegroundColor?.resolve({}))), + ); + expect(day24Shape.side.width, datePickerTheme.todayBorder?.width); // Test the day overlay color. final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');