From c6c3876ba9566e55c895ecbecfb564b499edfedb Mon Sep 17 00:00:00 2001 From: Kishan Rathore <34465683+rkishan516@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:39:29 +0530 Subject: [PATCH] Feat: Add yearShape property to DatePickerThemeData (#163909) Feat: Add yearShape property to DatePickerThemeData fixes: #163340 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Tong Mu --- .../lib/date_picker_template.dart | 1 + .../src/material/calendar_date_picker.dart | 16 ++- .../lib/src/material/date_picker_theme.dart | 33 +++++ .../test/material/date_picker_theme_test.dart | 128 ++++++++++++++++-- 4 files changed, 160 insertions(+), 18 deletions(-) diff --git a/dev/tools/gen_defaults/lib/date_picker_template.dart b/dev/tools/gen_defaults/lib/date_picker_template.dart index ae8c3861b59..bd22d2c1170 100644 --- a/dev/tools/gen_defaults/lib/date_picker_template.dart +++ b/dev/tools/gen_defaults/lib/date_picker_template.dart @@ -50,6 +50,7 @@ class _${blockName}DefaultsM3 extends DatePickerThemeData { // TODO(tahatesser): Update this to use token when gen_defaults // supports `CircleBorder` for fully rounded corners. dayShape: const WidgetStatePropertyAll(CircleBorder()), + yearShape: const WidgetStatePropertyAll(StadiumBorder()), rangePickerElevation: ${elevation("md.comp.date-picker.modal.range-selection.container")}, rangePickerShape: ${shape("md.comp.date-picker.modal.range-selection.container")}, ); diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index 0fcc60bd59e..d78da3cbbf5 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -1409,17 +1409,19 @@ class _YearPickerState extends State { effectiveValue((DatePickerThemeData? theme) => theme?.yearOverlayColor?.resolve(states)), ); - BoxBorder? border; + final OutlinedBorder yearShape = + resolve((DatePickerThemeData? theme) => theme?.yearShape, states)!; + + BorderSide? borderSide; if (isCurrentYear) { - final BorderSide? todayBorder = datePickerTheme.todayBorder ?? defaults.todayBorder; - if (todayBorder != null) { - border = Border.fromBorderSide(todayBorder.copyWith(color: textColor)); + borderSide = datePickerTheme.todayBorder ?? defaults.todayBorder; + if (borderSide != null) { + borderSide = borderSide.copyWith(color: textColor); } } - final BoxDecoration decoration = BoxDecoration( - border: border, + final ShapeDecoration decoration = ShapeDecoration( color: background, - borderRadius: BorderRadius.circular(decorationHeight / 2), + shape: yearShape.copyWith(side: borderSide), ); final TextStyle? itemStyle = (datePickerTheme.yearStyle ?? defaults.yearStyle)?.apply( diff --git a/packages/flutter/lib/src/material/date_picker_theme.dart b/packages/flutter/lib/src/material/date_picker_theme.dart index 855269c34c2..785a51d8240 100644 --- a/packages/flutter/lib/src/material/date_picker_theme.dart +++ b/packages/flutter/lib/src/material/date_picker_theme.dart @@ -67,6 +67,7 @@ class DatePickerThemeData with Diagnosticable { this.yearForegroundColor, this.yearBackgroundColor, this.yearOverlayColor, + this.yearShape, this.rangePickerBackgroundColor, this.rangePickerElevation, this.rangePickerShadowColor, @@ -251,6 +252,19 @@ class DatePickerThemeData with Diagnosticable { /// or pressed. final WidgetStateProperty? yearOverlayColor; + /// Overrides the default shape used to paint the shape decoration of the + /// year labels in the list of the year picker. + /// + /// If the selected year is the current year, the provided shape with the + /// value of [todayBackgroundColor] is used to paint the shape decoration of + /// the year label and the value of [todayBorder] and [todayForegroundColor] is + /// used to paint the border. + /// + /// If the selected year is not the current year, the provided shape with the + /// value of [yearBackgroundColor] is used to paint the shape decoration of + /// the year label. + final WidgetStateProperty? yearShape; + /// Overrides the default [Scaffold.backgroundColor] for /// [DateRangePickerDialog]. final Color? rangePickerBackgroundColor; @@ -384,6 +398,7 @@ class DatePickerThemeData with Diagnosticable { WidgetStateProperty? yearForegroundColor, WidgetStateProperty? yearBackgroundColor, WidgetStateProperty? yearOverlayColor, + WidgetStateProperty? yearShape, Color? rangePickerBackgroundColor, double? rangePickerElevation, Color? rangePickerShadowColor, @@ -424,6 +439,7 @@ class DatePickerThemeData with Diagnosticable { yearForegroundColor: yearForegroundColor ?? this.yearForegroundColor, yearBackgroundColor: yearBackgroundColor ?? this.yearBackgroundColor, yearOverlayColor: yearOverlayColor ?? this.yearOverlayColor, + yearShape: yearShape ?? this.yearShape, rangePickerBackgroundColor: rangePickerBackgroundColor ?? this.rangePickerBackgroundColor, rangePickerElevation: rangePickerElevation ?? this.rangePickerElevation, rangePickerShadowColor: rangePickerShadowColor ?? this.rangePickerShadowColor, @@ -520,6 +536,12 @@ class DatePickerThemeData with Diagnosticable { t, Color.lerp, ), + yearShape: WidgetStateProperty.lerp( + a?.yearShape, + b?.yearShape, + t, + OutlinedBorder.lerp, + ), rangePickerBackgroundColor: Color.lerp( a?.rangePickerBackgroundColor, b?.rangePickerBackgroundColor, @@ -606,6 +628,7 @@ class DatePickerThemeData with Diagnosticable { yearForegroundColor, yearBackgroundColor, yearOverlayColor, + yearShape, rangePickerBackgroundColor, rangePickerElevation, rangePickerShadowColor, @@ -652,6 +675,7 @@ class DatePickerThemeData with Diagnosticable { other.yearForegroundColor == yearForegroundColor && other.yearBackgroundColor == yearBackgroundColor && other.yearOverlayColor == yearOverlayColor && + other.yearShape == yearShape && other.rangePickerBackgroundColor == rangePickerBackgroundColor && other.rangePickerElevation == rangePickerElevation && other.rangePickerShadowColor == rangePickerShadowColor && @@ -765,6 +789,13 @@ class DatePickerThemeData with Diagnosticable { defaultValue: null, ), ); + properties.add( + DiagnosticsProperty>( + 'yearShape', + yearShape, + defaultValue: null, + ), + ); properties.add( ColorProperty('rangePickerBackgroundColor', rangePickerBackgroundColor, defaultValue: null), ); @@ -943,6 +974,7 @@ class _DatePickerDefaultsM2 extends DatePickerThemeData { elevation: 24.0, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))), dayShape: const WidgetStatePropertyAll(CircleBorder()), + yearShape: const WidgetStatePropertyAll(StadiumBorder()), rangePickerElevation: 0.0, rangePickerShape: const RoundedRectangleBorder(), ); @@ -1117,6 +1149,7 @@ class _DatePickerDefaultsM3 extends DatePickerThemeData { // TODO(tahatesser): Update this to use token when gen_defaults // supports `CircleBorder` for fully rounded corners. dayShape: const WidgetStatePropertyAll(CircleBorder()), + yearShape: const WidgetStatePropertyAll(StadiumBorder()), rangePickerElevation: 0.0, rangePickerShape: const RoundedRectangleBorder(), ); diff --git a/packages/flutter/test/material/date_picker_theme_test.dart b/packages/flutter/test/material/date_picker_theme_test.dart index 0b5afd1d274..7c25ef0b169 100644 --- a/packages/flutter/test/material/date_picker_theme_test.dart +++ b/packages/flutter/test/material/date_picker_theme_test.dart @@ -32,6 +32,7 @@ void main() { yearForegroundColor: MaterialStatePropertyAll(Color(0xfffffffa)), yearBackgroundColor: MaterialStatePropertyAll(Color(0xfffffffb)), yearOverlayColor: MaterialStatePropertyAll(Color(0xfffffffc)), + yearShape: MaterialStatePropertyAll(RoundedRectangleBorder()), rangePickerBackgroundColor: Color(0xfffffffd), rangePickerElevation: 7, rangePickerShadowColor: Color(0xfffffffe), @@ -69,11 +70,11 @@ void main() { ); } - BoxDecoration? findTextDecoration(WidgetTester tester, String date) { + ShapeDecoration? findTextDecoration(WidgetTester tester, String date) { final Container container = tester.widget( find.ancestor(of: find.text(date), matching: find.byType(Container)).first, ); - return container.decoration as BoxDecoration?; + return container.decoration as ShapeDecoration?; } ShapeDecoration? findDayDecoration(WidgetTester tester, String day) { @@ -449,6 +450,7 @@ void main() { equalsIgnoringHashCodes(TextButton.styleFrom().toString()), ); expect(m2.locale, null); + expect(m2.yearShape?.resolve({}), const StadiumBorder()); }); testWidgets('Default DatePickerThemeData debugFillProperties', (WidgetTester tester) async { @@ -500,6 +502,7 @@ void main() { 'yearForegroundColor: WidgetStatePropertyAll(${const Color(0xfffffffa)})', 'yearBackgroundColor: WidgetStatePropertyAll(${const Color(0xfffffffb)})', 'yearOverlayColor: WidgetStatePropertyAll(${const Color(0xfffffffc)})', + 'yearShape: WidgetStatePropertyAll(RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero))', 'rangePickerBackgroundColor: ${const Color(0xfffffffd)}', 'rangePickerElevation: 7.0', 'rangePickerShadowColor: ${const Color(0xfffffffe)}', @@ -607,30 +610,28 @@ void main() { await tester.pumpAndSettle(); final Text year2022 = tester.widget(find.text('2022')); - final BoxDecoration year2022Decoration = findTextDecoration(tester, '2022')!; + final ShapeDecoration year2022Decoration = findTextDecoration(tester, '2022')!; expect(year2022.style?.fontSize, datePickerTheme.yearStyle?.fontSize); expect(year2022.style?.color, datePickerTheme.yearForegroundColor?.resolve({})); expect( year2022Decoration.color, datePickerTheme.yearBackgroundColor?.resolve({}), ); + expect(year2022Decoration.shape, datePickerTheme.yearShape?.resolve({})); final Text year2023 = tester.widget(find.text('2023')); // DatePickerDialog.currentDate - final BoxDecoration year2023Decoration = findTextDecoration(tester, '2023')!; + final ShapeDecoration year2023Decoration = findTextDecoration(tester, '2023')!; expect(year2023.style?.fontSize, datePickerTheme.yearStyle?.fontSize); expect(year2023.style?.color, datePickerTheme.todayForegroundColor?.resolve({})); expect( year2023Decoration.color, datePickerTheme.todayBackgroundColor?.resolve({}), ); - expect(year2023Decoration.border?.top.width, datePickerTheme.todayBorder?.width); - expect(year2023Decoration.border?.bottom.width, datePickerTheme.todayBorder?.width); + final RoundedRectangleBorder roundedRectangleBorder = + year2023Decoration.shape as RoundedRectangleBorder; + expect(roundedRectangleBorder.side.width, datePickerTheme.todayBorder?.width); expect( - year2023Decoration.border?.top.color, - datePickerTheme.todayForegroundColor?.resolve({}), - ); - expect( - year2023Decoration.border?.bottom.color, + roundedRectangleBorder.side.color, datePickerTheme.todayForegroundColor?.resolve({}), ); @@ -1206,4 +1207,109 @@ void main() { ); } }); + + testWidgets('YearPicker maintains default year shape at textScaleFactor 1, 1.5, 2', ( + WidgetTester tester, + ) async { + double textScaleFactor = 1.0; + Widget buildFrame() { + return MaterialApp( + home: Builder( + builder: (BuildContext context) { + return MediaQuery.withClampedTextScaling( + minScaleFactor: textScaleFactor, + maxScaleFactor: textScaleFactor, + child: Scaffold( + body: YearPicker( + currentDate: DateTime(2025), + firstDate: DateTime(2021), + lastDate: DateTime(2030), + selectedDate: DateTime(2025), + onChanged: (DateTime value) {}, + ), + ), + ); + }, + ), + ); + } + + await tester.pumpWidget(buildFrame()); + + // Find container whose child is text 2025. + final Finder yearContainer = + find.ancestor(of: find.text('2025'), matching: find.byType(Container)).first; + + expect( + tester.renderObject(yearContainer), + paints..rrect( + rrect: RRect.fromLTRBR(0.5, 0.5, 71.5, 35.5, const Radius.circular(17.5)), + color: const Color(0xFF6750A4), + ), + ); + + textScaleFactor = 1.5; + await tester.pumpWidget(buildFrame()); + + expect( + tester.renderObject(yearContainer), + paints..rrect( + rrect: RRect.fromLTRBR(0.5, 0.5, 107.5, 51.5, const Radius.circular(25.5)), + color: const Color(0xFF6750A4), + ), + ); + + textScaleFactor = 2; + await tester.pumpWidget(buildFrame()); + + expect( + tester.renderObject(yearContainer), + paints..rrect( + rrect: RRect.fromLTRBR(0.5, 0.5, 143.5, 51.5, const Radius.circular(25.5)), + color: const Color(0xFF6750A4), + ), + ); + }); + + testWidgets('YearPicker applies shape from DatePickerThemeData.yearShape correctly', ( + WidgetTester tester, + ) async { + const OutlinedBorder yearShpae = CircleBorder(); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + datePickerTheme: datePickerTheme.copyWith( + yearShape: MaterialStateProperty.all(yearShpae), + ), + ), + home: Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: Center( + child: YearPicker( + currentDate: DateTime(2025), + firstDate: DateTime(2021), + lastDate: DateTime(2030), + selectedDate: DateTime(2025), + onChanged: (DateTime value) {}, + ), + ), + ), + ), + ), + ); + + final ShapeDecoration year2022Decoration = findTextDecoration(tester, '2022')!; + final OutlinedBorder year2022roundedRectangleBorder = year2022Decoration.shape as CircleBorder; + expect(year2022roundedRectangleBorder.side.width, 0.0); + expect(year2022roundedRectangleBorder.side.color, yearShpae.side.color); + + final ShapeDecoration year2025Decoration = findTextDecoration(tester, '2025')!; + final OutlinedBorder year2022RoundedRectangleBorder = year2025Decoration.shape as CircleBorder; + expect(year2022RoundedRectangleBorder.side.width, datePickerTheme.todayBorder?.width); + expect( + year2022RoundedRectangleBorder.side.color, + datePickerTheme.todayForegroundColor?.resolve({}), + ); + }); }