mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[Material] Add support for high contrast theming to Material apps (#62337)
This commit is contained in:
parent
e9e36f3909
commit
0a69e810b1
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user