Unverified Commit 0a69e810 authored by Rami's avatar Rami Committed by GitHub

[Material] Add support for high contrast theming to Material apps (#62337)

parent e9e36f39
......@@ -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<MaterialApp> {
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,
......
......@@ -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
......
......@@ -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) {
......
......@@ -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;
......
......@@ -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);
});
}
......@@ -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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment