diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index c11bff495bf..3bfbc8dc31d 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -180,6 +180,8 @@ class MaterialApp extends StatefulWidget { this.color, this.theme, this.darkTheme, + this.highContrastTheme, + this.highContrastDarkTheme, this.themeMode = ThemeMode.system, this.locale, this.localizationsDelegates, @@ -294,6 +296,35 @@ class MaterialApp extends StatefulWidget { /// [MediaQueryData.platformBrightness]. final ThemeData darkTheme; + /// The [ThemeData] to use when 'high contrast' is requested by the system. + /// + /// Some host platforms (for example, iOS) allow the users to increase + /// contrast through an accessibility setting. + /// + /// Uses [theme] instead when null. + /// + /// See also: + /// + /// * [MediaQueryData.highContrast], which indicates the platform's + /// desire to increase contrast. + final ThemeData highContrastTheme; + + /// The [ThemeData] to use when a 'dark mode' and 'high contrast' is requested + /// by the system. + /// + /// Some host platforms (for example, iOS) allow the users to increase + /// contrast through an accessibility setting. + /// + /// This theme should have a [ThemeData.brightness] set to [Brightness.dark]. + /// + /// Uses [darkTheme] instead when null. + /// + /// See also: + /// + /// * [MediaQueryData.highContrast], which indicates the platform's + /// desire to increase contrast. + final ThemeData highContrastDarkTheme; + /// Determines which theme will be used by the application if both [theme] /// and [darkTheme] are provided. /// @@ -616,17 +647,22 @@ class _MaterialAppState extends State { onGenerateInitialRoutes: widget.onGenerateInitialRoutes, onUnknownRoute: widget.onUnknownRoute, builder: (BuildContext context, Widget child) { - // Use a light theme, dark theme, or fallback theme. + // Resolve which theme to use based on brightness and high contrast. final ThemeMode mode = widget.themeMode ?? ThemeMode.system; + final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context); + final bool useDarkTheme = mode == ThemeMode.dark + || (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark); + final bool highContrast = MediaQuery.highContrastOf(context); ThemeData theme; - if (widget.darkTheme != null) { - final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context); - if (mode == ThemeMode.dark || - (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark)) { - theme = widget.darkTheme; - } + + if (useDarkTheme && highContrast) { + theme = widget.highContrastDarkTheme; + } else if (useDarkTheme) { + theme = widget.darkTheme; + } else if (highContrast) { + theme = widget.highContrastTheme; } - theme ??= widget.theme ?? ThemeData.fallback(); + theme ??= widget.theme ?? ThemeData.light(); return AnimatedTheme( data: theme, diff --git a/packages/flutter/lib/src/material/color_scheme.dart b/packages/flutter/lib/src/material/color_scheme.dart index 647964eda67..93e95e6676a 100644 --- a/packages/flutter/lib/src/material/color_scheme.dart +++ b/packages/flutter/lib/src/material/color_scheme.dart @@ -108,6 +108,67 @@ class ColorScheme with Diagnosticable { assert(onError != null), assert(brightness != null); + + /// Create a high contrast ColorScheme based on a purple primary color that + /// matches the [baseline Material color scheme](https://material.io/design/color/the-color-system.html#color-theme-creation). + const ColorScheme.highContrastLight({ + this.primary = const Color(0xff0000ba), + this.primaryVariant = const Color(0xff000088), + this.secondary = const Color(0xff66fff9), + this.secondaryVariant = const Color(0xff018786), + this.surface = Colors.white, + this.background = Colors.white, + this.error = const Color(0xff790000), + this.onPrimary = Colors.white, + this.onSecondary = Colors.black, + this.onSurface = Colors.black, + this.onBackground = Colors.black, + this.onError = Colors.white, + this.brightness = Brightness.light, + }) : assert(primary != null), + assert(primaryVariant != null), + assert(secondary != null), + assert(secondaryVariant != null), + assert(surface != null), + assert(background != null), + assert(error != null), + assert(onPrimary != null), + assert(onSecondary != null), + assert(onSurface != null), + assert(onBackground != null), + assert(onError != null), + assert(brightness != null); + + /// Create a high contrast ColorScheme based on the dark + /// [baseline Material color scheme](https://material.io/design/color/dark-theme.html#ui-application). + const ColorScheme.highContrastDark({ + this.primary = const Color(0xffefb7ff), + this.primaryVariant = const Color(0xffbe9eff), + this.secondary = const Color(0xff66fff9), + this.secondaryVariant = const Color(0xff66fff9), + this.surface = const Color(0xff121212), + this.background = const Color(0xff121212), + this.error = const Color(0xff9b374d), + this.onPrimary = Colors.black, + this.onSecondary = Colors.black, + this.onSurface = Colors.white, + this.onBackground = Colors.white, + this.onError = Colors.black, + this.brightness = Brightness.dark, + }) : assert(primary != null), + assert(primaryVariant != null), + assert(secondary != null), + assert(secondaryVariant != null), + assert(surface != null), + assert(background != null), + assert(error != null), + assert(onPrimary != null), + assert(onSecondary != null), + assert(onSurface != null), + assert(onBackground != null), + assert(onError != null), + assert(brightness != null); + /// Create a color scheme from a [MaterialColor] swatch. /// /// This constructor is used by [ThemeData] to create its default diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index 28423f895fc..9cb72cbcc39 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -843,6 +843,17 @@ class MediaQuery extends InheritedWidget { return MediaQuery.of(context, nullOk: true)?.platformBrightness ?? Brightness.light; } + /// Returns highContrast for the nearest MediaQuery ancestor or false, if no + /// such ancestor exists. + /// + /// See also: + /// + /// * [MediaQueryData.highContrast], which indicates the platform's + /// desire to increase contrast. + static bool highContrastOf(BuildContext context) { + return MediaQuery.of(context, nullOk: true)?.highContrast ?? false; + } + /// Returns the boldText accessibility setting for the nearest MediaQuery /// ancestor, or false if no such ancestor exists. static bool boldTextOverride(BuildContext context) { diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 15fb11bd1c7..7a1ac44dd1a 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -752,6 +752,66 @@ void main() { expect(appliedTheme.brightness, Brightness.dark); }); + testWidgets('MaterialApp uses high contrast theme when appropriate', (WidgetTester tester) async { + tester.binding.window.platformBrightnessTestValue = Brightness.light; + tester.binding.window.accessibilityFeaturesTestValue = MockAccessibilityFeature(); + + ThemeData appliedTheme; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + primaryColor: Colors.lightBlue, + ), + highContrastTheme: ThemeData( + primaryColor: Colors.blue, + ), + home: Builder( + builder: (BuildContext context) { + appliedTheme = Theme.of(context); + return const SizedBox(); + }, + ), + ), + ); + + expect(appliedTheme.primaryColor, Colors.blue); + tester.binding.window.accessibilityFeaturesTestValue = null; + }); + + testWidgets('MaterialApp uses high contrast dark theme when appropriate', (WidgetTester tester) async { + tester.binding.window.platformBrightnessTestValue = Brightness.dark; + tester.binding.window.accessibilityFeaturesTestValue = MockAccessibilityFeature(); + + ThemeData appliedTheme; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + primaryColor: Colors.lightBlue, + ), + darkTheme: ThemeData( + primaryColor: Colors.lightGreen, + ), + highContrastTheme: ThemeData( + primaryColor: Colors.blue, + ), + highContrastDarkTheme: ThemeData( + primaryColor: Colors.green, + ), + home: Builder( + builder: (BuildContext context) { + appliedTheme = Theme.of(context); + return const SizedBox(); + }, + ), + ), + ); + + expect(appliedTheme.primaryColor, Colors.green); + tester.binding.window.accessibilityFeaturesTestValue = null; + }); + testWidgets('MaterialApp switches themes when the Window platformBrightness changes.', (WidgetTester tester) async { // Mock the Window to explicitly report a light platformBrightness. final TestWidgetsFlutterBinding binding = tester.binding; diff --git a/packages/flutter/test/material/color_scheme_test.dart b/packages/flutter/test/material/color_scheme_test.dart index 351ebdcb1c6..1fb09f2189b 100644 --- a/packages/flutter/test/material/color_scheme_test.dart +++ b/packages/flutter/test/material/color_scheme_test.dart @@ -45,4 +45,42 @@ void main() { expect(scheme.onError, const Color(0xff000000)); expect(scheme.brightness, Brightness.dark); }); + + test('high contrast light scheme matches the spec', () { + // Colors are based off of the Material Design baseline default theme: + // https://material.io/design/color/dark-theme.html#ui-application + const ColorScheme scheme = ColorScheme.highContrastLight(); + expect(scheme.primary, const Color(0xff0000ba)); + expect(scheme.primaryVariant, const Color(0xff000088)); + expect(scheme.secondary, const Color(0xff66fff9)); + expect(scheme.secondaryVariant, const Color(0xff018786)); + expect(scheme.background, const Color(0xffffffff)); + expect(scheme.surface, const Color(0xffffffff)); + expect(scheme.error, const Color(0xff790000)); + expect(scheme.onPrimary, const Color(0xffffffff)); + expect(scheme.onSecondary, const Color(0xff000000)); + expect(scheme.onBackground, const Color(0xff000000)); + expect(scheme.onSurface, const Color(0xff000000)); + expect(scheme.onError, const Color(0xffffffff)); + expect(scheme.brightness, Brightness.light); + }); + + test('high contrast dark scheme matches the spec', () { + // Colors are based off of the Material Design baseline dark theme: + // https://material.io/design/color/dark-theme.html#ui-application + const ColorScheme scheme = ColorScheme.highContrastDark(); + expect(scheme.primary, const Color(0xffefb7ff)); + expect(scheme.primaryVariant, const Color(0xffbe9eff)); + expect(scheme.secondary, const Color(0xff66fff9)); + expect(scheme.secondaryVariant, const Color(0xff66fff9)); + expect(scheme.background, const Color(0xff121212)); + expect(scheme.surface, const Color(0xff121212)); + expect(scheme.error, const Color(0xff9b374d)); + expect(scheme.onPrimary, const Color(0xff000000)); + expect(scheme.onSecondary, const Color(0xff000000)); + expect(scheme.onBackground, const Color(0xffffffff)); + expect(scheme.onSurface, const Color(0xffffffff)); + expect(scheme.onError, const Color(0xff000000)); + expect(scheme.brightness, Brightness.dark); + }); } diff --git a/packages/flutter/test/widgets/media_query_test.dart b/packages/flutter/test/widgets/media_query_test.dart index cb7ceb875ed..b36e03361a2 100644 --- a/packages/flutter/test/widgets/media_query_test.dart +++ b/packages/flutter/test/widgets/media_query_test.dart @@ -535,6 +535,33 @@ void main() { expect(insideBrightness, Brightness.dark); }); + testWidgets('MediaQuery.highContrastOf', (WidgetTester tester) async { + bool outsideHighContrast; + bool insideHighContrast; + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + outsideHighContrast = MediaQuery.highContrastOf(context); + return MediaQuery( + data: const MediaQueryData( + highContrast: true, + ), + child: Builder( + builder: (BuildContext context) { + insideHighContrast = MediaQuery.highContrastOf(context); + return Container(); + }, + ), + ); + }, + ), + ); + + expect(outsideHighContrast, false); + expect(insideHighContrast, true); + }); + testWidgets('MediaQuery.boldTextOverride', (WidgetTester tester) async { bool outsideBoldTextOverride; bool insideBoldTextOverride;