Skip to content

Commit

Permalink
Fix CalendarDatePicker day selection shape and overlay (#144317)
Browse files Browse the repository at this point in the history
fixes [`DatePickerDialog` date entry hover background and ink splash have different radius](#141350)
fixes [Ability to customize DatePicker day selection background and overlay shape](#144220)

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @OverRide
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Builder(builder: (context) {
            return FilledButton(
              onPressed: () {
                showDatePicker(
                  context: context,
                  initialDate: DateTime.now(),
                  firstDate: DateTime.utc(2010),
                  lastDate: DateTime.utc(2030),
                );
              },
              child: const Text('Show Date picker'),
            );
          }),
        ),
      ),
    );
  }
}
```

</details>

### Material DatePicker states specs

![overlay_specs](https://github.com/flutter/flutter/assets/48603081/45ce16cf-7ee9-41e1-a4fa-327de07b78d1)

### Day selection overlay

| Before | After |
| --------------- | --------------- |
| <img src="https://github.com/flutter/flutter/assets/48603081/b529d38d-0232-494b-8bf2-55d28420a245" /> | <img src="https://github.com/flutter/flutter/assets/48603081/c4799559-a7ef-45fd-aed9-aeb386370580"  /> |

### Hover, pressed, highlight preview

| Before | After |
| --------------- | --------------- |
| <video src="https://github.com/flutter/flutter/assets/48603081/8edde82a-7f39-4482-afab-183e1bce5991" /> | <video src="https://github.com/flutter/flutter/assets/48603081/04e1502e-67a4-4b33-973d-463067d70151" /> |

### Using `DatePickerThemeData.dayShape` to customize day selection background and overlay shape

| Before | After |
| --------------- | --------------- |
| <img src="https://github.com/flutter/flutter/assets/48603081/a0c85f58-a69b-4e14-a45d-41e580ceedce"  />  | <img src="https://github.com/flutter/flutter/assets/48603081/db67cee1-d28d-4168-98b8-fd7a9cb70cda" /> | 

### Example preview

![Screenshot 2024-02-29 at 15 07 50](https://github.com/flutter/flutter/assets/48603081/3770ed5c-28bf-4d0a-9514-87e1cd2ce515)
  • Loading branch information
TahaTesser committed Mar 1, 2024
1 parent cfabdca commit ba719bc
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 15 deletions.
3 changes: 3 additions & 0 deletions dev/tools/gen_defaults/lib/date_picker_template.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<OutlinedBorder>(CircleBorder()),
rangePickerElevation: ${elevation("md.comp.date-picker.modal.range-selection.container")},
rangePickerShape: ${shape("md.comp.date-picker.modal.range-selection.container")},
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Color>(Colors.amber),
todayForegroundColor: const MaterialStatePropertyAll<Color>(Colors.black),
todayBorder: const BorderSide(width: 2),
dayShape: MaterialStatePropertyAll<OutlinedBorder>(
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<DatePickerExample> createState() => _DatePickerExampleState();
}

class _DatePickerExampleState extends State<DatePickerExample> {
@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'),
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -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<DecoratedBox>(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<DecoratedBox>(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<DecoratedBox>(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)));
});
}
21 changes: 11 additions & 10 deletions packages/flutter/lib/src/material/calendar_date_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1057,21 +1057,21 @@ class _DayState extends State<_Day> {
final MaterialStateProperty<Color?> dayOverlayColor = MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) => effectiveValue((DatePickerThemeData? theme) => theme?.dayOverlayColor?.resolve(states)),
);
final BoxDecoration decoration = widget.isToday
? BoxDecoration(
final OutlinedBorder dayShape = resolve<OutlinedBorder?>((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)),
Expand All @@ -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
Expand Down
48 changes: 48 additions & 0 deletions packages/flutter/lib/src/material/date_picker_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class DatePickerThemeData with Diagnosticable {
this.dayForegroundColor,
this.dayBackgroundColor,
this.dayOverlayColor,
this.dayShape,
this.todayForegroundColor,
this.todayBackgroundColor,
this.todayBorder,
Expand Down Expand Up @@ -163,12 +164,41 @@ class DatePickerThemeData with Diagnosticable {
/// indicate that a day in the grid is focused, hovered, or pressed.
final MaterialStateProperty<Color?>? 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<OutlinedBorder?>? 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<Color?>? todayForegroundColor;

/// Overrides the default color used to paint the background of the
Expand All @@ -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
Expand Down Expand Up @@ -326,6 +364,7 @@ class DatePickerThemeData with Diagnosticable {
MaterialStateProperty<Color?>? dayForegroundColor,
MaterialStateProperty<Color?>? dayBackgroundColor,
MaterialStateProperty<Color?>? dayOverlayColor,
MaterialStateProperty<OutlinedBorder?>? dayShape,
MaterialStateProperty<Color?>? todayForegroundColor,
MaterialStateProperty<Color?>? todayBackgroundColor,
BorderSide? todayBorder,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -409,6 +449,7 @@ class DatePickerThemeData with Diagnosticable {
dayForegroundColor: MaterialStateProperty.lerp<Color?>(a?.dayForegroundColor, b?.dayForegroundColor, t, Color.lerp),
dayBackgroundColor: MaterialStateProperty.lerp<Color?>(a?.dayBackgroundColor, b?.dayBackgroundColor, t, Color.lerp),
dayOverlayColor: MaterialStateProperty.lerp<Color?>(a?.dayOverlayColor, b?.dayOverlayColor, t, Color.lerp),
dayShape: MaterialStateProperty.lerp<OutlinedBorder?>(a?.dayShape, b?.dayShape, t, OutlinedBorder.lerp),
todayForegroundColor: MaterialStateProperty.lerp<Color?>(a?.todayForegroundColor, b?.todayForegroundColor, t, Color.lerp),
todayBackgroundColor: MaterialStateProperty.lerp<Color?>(a?.todayBackgroundColor, b?.todayBackgroundColor, t, Color.lerp),
todayBorder: _lerpBorderSide(a?.todayBorder, b?.todayBorder, t),
Expand Down Expand Up @@ -460,6 +501,7 @@ class DatePickerThemeData with Diagnosticable {
dayForegroundColor,
dayBackgroundColor,
dayOverlayColor,
dayShape,
todayForegroundColor,
todayBackgroundColor,
todayBorder,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -545,6 +588,7 @@ class DatePickerThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('dayForegroundColor', dayForegroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('dayBackgroundColor', dayBackgroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('dayOverlayColor', dayOverlayColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<OutlinedBorder?>>('dayShape', dayShape, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('todayForegroundColor', todayForegroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('todayBackgroundColor', todayBackgroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<BorderSide?>('todayBorder', todayBorder, defaultValue: null));
Expand Down Expand Up @@ -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<OutlinedBorder>(CircleBorder()),
rangePickerElevation: 0.0,
rangePickerShape: const RoundedRectangleBorder(),
);
Expand Down Expand Up @@ -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<OutlinedBorder>(CircleBorder()),
rangePickerElevation: 0.0,
rangePickerShape: const RoundedRectangleBorder(),
);
Expand Down
41 changes: 40 additions & 1 deletion packages/flutter/test/material/calendar_date_picker_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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', () {
Expand Down
Loading

0 comments on commit ba719bc

Please sign in to comment.