Add contrastLevel parameter to ColorScheme.fromSeed (#149705)

This PR is to add a parameter `contrastLevel` to `ColorScheme.fromSeed` so that we can construct high contrast `ColorScheme`.

https://github.com/flutter/flutter/assets/36861262/c609c996-5dfe-4c6c-800c-349a99de4256

Related to https://github.com/flutter/flutter/issues/149683
This commit is contained in:
Qun Cheng 2024-06-05 22:16:08 +00:00 committed by GitHub
parent d46463056c
commit 3ed3f31ba6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 267 additions and 90 deletions

View File

@ -20,8 +20,17 @@ class ColorSchemeExample extends StatefulWidget {
class _ColorSchemeExampleState extends State<ColorSchemeExample> {
Color selectedColor = ColorSeed.baseColor.color;
Brightness selectedBrightness = Brightness.light;
double selectedContrast = 0.0;
static const List<DynamicSchemeVariant> schemeVariants = DynamicSchemeVariant.values;
void updateTheme(Brightness brightness, Color color, double contrastLevel) {
setState(() {
selectedBrightness = brightness;
selectedColor = color;
selectedContrast = contrastLevel;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
@ -30,87 +39,152 @@ class _ColorSchemeExampleState extends State<ColorSchemeExample> {
colorScheme: ColorScheme.fromSeed(
seedColor: selectedColor,
brightness: selectedBrightness,
contrastLevel: selectedContrast,
)
),
home: Builder(
builder: (BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('ColorScheme'),
actions: <Widget>[
home: Scaffold(
appBar: AppBar(
title: const Text('ColorScheme'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
showModalBottomSheet<void>(
barrierColor: Colors.transparent,
context: context,
builder: (BuildContext context) => Settings(
selectedColor: selectedColor,
selectedBrightness: selectedBrightness,
selectedContrast: selectedContrast,
updateTheme: updateTheme
)
);
},
),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List<Widget>.generate(schemeVariants.length, (int index) {
return ColorSchemeVariantColumn(
selectedColor: selectedColor,
brightness: selectedBrightness,
schemeVariant: schemeVariants[index],
contrastLevel: selectedContrast,
);
}).toList(),
),
),
],
),
),
),
),
);
}
}
class Settings extends StatefulWidget {
const Settings({
super.key,
required this.updateTheme,
required this.selectedBrightness,
required this.selectedContrast,
required this.selectedColor,
});
final Brightness selectedBrightness;
final double selectedContrast;
final Color selectedColor;
final void Function(Brightness, Color, double) updateTheme;
@override
State<Settings> createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
late Brightness selectedBrightness = widget.selectedBrightness;
late Color selectedColor = widget.selectedColor;
late double selectedContrast = widget.selectedContrast;
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context).copyWith(colorScheme: ColorScheme.fromSeed(
seedColor: selectedColor,
contrastLevel: selectedContrast,
brightness: selectedBrightness,
)),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: ListView(
children: <Widget>[
Center(child: Text('Settings', style: Theme.of(context).textTheme.titleLarge)),
Row(
children: <Widget>[
const Text('Color Seed'),
MenuAnchor(
builder: (BuildContext context, MenuController controller, Widget? widget) {
return IconButton(
icon: Icon(Icons.circle, color: selectedColor),
onPressed: () {
setState(() {
if (!controller.isOpen) {
controller.open();
}
});
},
);
const Text('Brightness: '),
Switch(
value: selectedBrightness == Brightness.light,
onChanged: (bool value) {
setState(() {
selectedBrightness = value ? Brightness.light : Brightness.dark;
});
widget.updateTheme.call(selectedBrightness, selectedColor, selectedContrast);
},
menuChildren: List<Widget>.generate(ColorSeed.values.length, (int index) {
final Color itemColor = ColorSeed.values[index].color;
return MenuItemButton(
leadingIcon: selectedColor == ColorSeed.values[index].color
? Icon(Icons.circle, color: itemColor)
: Icon(Icons.circle_outlined, color: itemColor),
onPressed: () {
setState(() {
selectedColor = itemColor;
});
},
child: Text(ColorSeed.values[index].label),
);
}),
)
],
),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
const Text('Seed color: '),
...List<Widget>.generate(ColorSeed.values.length, (int index) {
final Color itemColor = ColorSeed.values[index].color;
return IconButton(
icon: selectedColor == ColorSeed.values[index].color
? Icon(Icons.circle, color: itemColor)
: Icon(Icons.circle_outlined, color: itemColor),
onPressed: () {
setState(() {
selectedColor = itemColor;
});
widget.updateTheme.call(selectedBrightness, selectedColor, selectedContrast);
},
);
}),
]
),
Row(
children: <Widget>[
const Text('Contrast level: '),
Expanded(
child: Slider(
divisions: 4,
label: selectedContrast.toString(),
min: -1,
value: selectedContrast,
onChanged: (double value) {
setState(() {
selectedContrast = value;
});
widget.updateTheme.call(selectedBrightness, selectedColor, selectedContrast);
},
),
),
],
),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
children: <Widget>[
const Text('Brightness'),
const SizedBox(width: 10),
Switch(
value: selectedBrightness == Brightness.light,
onChanged: (bool value) {
setState(() {
selectedBrightness = value ? Brightness.light : Brightness.dark;
});
},
),
],
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List<Widget>.generate(schemeVariants.length, (int index) {
return ColorSchemeVariantColumn(
selectedColor: selectedColor,
brightness: selectedBrightness,
schemeVariant: schemeVariants[index],
);
}).toList(),
),
),
],
),
),
),
),
),
);
@ -122,11 +196,13 @@ class ColorSchemeVariantColumn extends StatelessWidget {
super.key,
this.schemeVariant = DynamicSchemeVariant.tonalSpot,
this.brightness = Brightness.light,
this.contrastLevel = 0.0,
required this.selectedColor,
});
final DynamicSchemeVariant schemeVariant;
final Brightness brightness;
final double contrastLevel;
final Color selectedColor;
@override
@ -148,6 +224,7 @@ class ColorSchemeVariantColumn extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(
seedColor: selectedColor,
brightness: brightness,
contrastLevel: contrastLevel,
dynamicSchemeVariant: schemeVariant,
),
),

View File

@ -9,7 +9,8 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('ColorScheme Smoke Test', (WidgetTester tester) async {
await tester.pumpWidget(
const example.ColorSchemeExample(),
const MaterialApp(home: example.ColorSchemeExample()
),
);
expect(find.text('tonalSpot (Default)'), findsOneWidget);
@ -18,7 +19,7 @@ void main() {
testWidgets('Change color seed', (WidgetTester tester) async {
await tester.pumpWidget(
const example.ColorSchemeExample(),
const MaterialApp(home: example.ColorSchemeExample()),
);
ColoredBox coloredBox() {
@ -30,9 +31,9 @@ void main() {
);
}
expect(coloredBox().color, const Color(0xff65558f));
await tester.tap(find.byType(MenuAnchor));
await tester.tap(find.byIcon(Icons.settings));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(MenuItemButton, 'Yellow'));
await tester.tap(find.byType(IconButton).at(6));
await tester.pumpAndSettle();
expect(coloredBox().color, const Color(0xFF685F12));

View File

@ -283,9 +283,15 @@ class ColorScheme with Diagnosticable {
/// If the resulting color scheme is too dark, consider setting `dynamicSchemeVariant`
/// to [DynamicSchemeVariant.fidelity], whose palettes match the seed color.
///
/// The `contrastLevel` parameter indicates the contrast level between color
/// pairs, such as [primary] and [onPrimary]. 0.0 is the default (normal);
/// -1.0 is the lowest; 1.0 is the highest. From Material Design guideline, the
/// medium and high contrast correspond to 0.5 and 1.0 respectively.
///
/// {@tool dartpad}
/// This sample shows how to use [ColorScheme.fromSeed] to create dynamic
/// color schemes with different [DynamicSchemeVariant]s.
/// color schemes with different [DynamicSchemeVariant]s and different
/// contrast level.
///
/// ** See code in examples/api/lib/material/color_scheme/color_scheme.0.dart **
/// {@end-tool}
@ -300,6 +306,7 @@ class ColorScheme with Diagnosticable {
required Color seedColor,
Brightness brightness = Brightness.light,
DynamicSchemeVariant dynamicSchemeVariant = DynamicSchemeVariant.tonalSpot,
double contrastLevel = 0.0,
Color? primary,
Color? onPrimary,
Color? primaryContainer,
@ -362,7 +369,7 @@ class ColorScheme with Diagnosticable {
)
Color? surfaceVariant,
}) {
final DynamicScheme scheme = _buildDynamicScheme(brightness, seedColor, dynamicSchemeVariant);
final DynamicScheme scheme = _buildDynamicScheme(brightness, seedColor, dynamicSchemeVariant, contrastLevel);
return ColorScheme(
primary: primary ?? Color(MaterialDynamicColors.primary.getArgb(scheme)),
@ -688,7 +695,8 @@ class ColorScheme with Diagnosticable {
/// This constructor shouldn't be used to update the Material 3 color scheme.
///
/// For Material 3, use [ColorScheme.fromSeed] to create a color scheme
/// from a single seed color based on the Material 3 color system.
/// from a single seed color based on the Material 3 color system. To create a
/// high-contrast color scheme, set `contrastLevel` to 1.0.
///
/// {@tool snippet}
/// This example demonstrates how to create a color scheme similar to [ColorScheme.highContrastLight]
@ -819,7 +827,8 @@ class ColorScheme with Diagnosticable {
/// For Material 3, use [ColorScheme.fromSeed] to create a color scheme
/// from a single seed color based on the Material 3 color system.
/// Override the `brightness` property of [ColorScheme.fromSeed] to create a
/// dark color scheme.
/// dark color scheme. To create a high-contrast color scheme, set
/// `contrastLevel` to 1.0.
///
/// {@tool snippet}
/// This example demonstrates how to create a color scheme similar to [ColorScheme.highContrastDark]
@ -1672,6 +1681,7 @@ class ColorScheme with Diagnosticable {
required ImageProvider provider,
Brightness brightness = Brightness.light,
DynamicSchemeVariant dynamicSchemeVariant = DynamicSchemeVariant.tonalSpot,
double contrastLevel = 0.0,
Color? primary,
Color? onPrimary,
Color? primaryContainer,
@ -1745,7 +1755,7 @@ class ColorScheme with Diagnosticable {
final List<int> scoredResults = Score.score(colorToCount, desired: 1);
final ui.Color baseColor = Color(scoredResults.first);
final DynamicScheme scheme = _buildDynamicScheme(brightness, baseColor, dynamicSchemeVariant);
final DynamicScheme scheme = _buildDynamicScheme(brightness, baseColor, dynamicSchemeVariant, contrastLevel);
return ColorScheme(
primary: primary ?? Color(MaterialDynamicColors.primary.getArgb(scheme)),
@ -1882,19 +1892,24 @@ class ColorScheme with Diagnosticable {
return (abgr & exceptRMask & exceptBMask) | (b << 16) | r;
}
static DynamicScheme _buildDynamicScheme(Brightness brightness, Color seedColor, DynamicSchemeVariant schemeVariant) {
static DynamicScheme _buildDynamicScheme(
Brightness brightness,
Color seedColor,
DynamicSchemeVariant schemeVariant,
double contrastLevel,
) {
final bool isDark = brightness == Brightness.dark;
final Hct sourceColor = Hct.fromInt(seedColor.value);
return switch (schemeVariant) {
DynamicSchemeVariant.tonalSpot => SchemeTonalSpot(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0),
DynamicSchemeVariant.fidelity => SchemeFidelity(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0),
DynamicSchemeVariant.content => SchemeContent(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0),
DynamicSchemeVariant.monochrome => SchemeMonochrome(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0),
DynamicSchemeVariant.neutral => SchemeNeutral(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0),
DynamicSchemeVariant.vibrant => SchemeVibrant(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0),
DynamicSchemeVariant.expressive => SchemeExpressive(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0),
DynamicSchemeVariant.rainbow => SchemeRainbow(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0),
DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: 0.0),
DynamicSchemeVariant.tonalSpot => SchemeTonalSpot(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.fidelity => SchemeFidelity(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.content => SchemeContent(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.monochrome => SchemeMonochrome(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.neutral => SchemeNeutral(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.vibrant => SchemeVibrant(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.expressive => SchemeExpressive(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.rainbow => SchemeRainbow(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
};
}
}

View File

@ -798,6 +798,90 @@ void main() {
const Color(0xFF5D5F5F)
);
});
testWidgets('Colors in high-contrast color scheme matches colors in DynamicScheme', (WidgetTester tester) async {
const Color seedColor = Colors.blue;
final Hct sourceColor = Hct.fromInt(seedColor.value);
void colorsMatchDynamicSchemeColors(DynamicSchemeVariant schemeVariant, Brightness brightness, double contrastLevel) {
final bool isDark = brightness == Brightness.dark;
final DynamicScheme dynamicScheme = switch (schemeVariant) {
DynamicSchemeVariant.tonalSpot => SchemeTonalSpot(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.fidelity => SchemeFidelity(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.content => SchemeContent(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.monochrome => SchemeMonochrome(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.neutral => SchemeNeutral(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.vibrant => SchemeVibrant(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.expressive => SchemeExpressive(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.rainbow => SchemeRainbow(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel),
};
final ColorScheme colorScheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: brightness,
dynamicSchemeVariant: schemeVariant,
contrastLevel: contrastLevel,
);
expect(colorScheme.primary.value, MaterialDynamicColors.primary.getArgb(dynamicScheme));
expect(colorScheme.onPrimary.value, MaterialDynamicColors.onPrimary.getArgb(dynamicScheme));
expect(colorScheme.primaryContainer.value, MaterialDynamicColors.primaryContainer.getArgb(dynamicScheme));
expect(colorScheme.onPrimaryContainer.value, MaterialDynamicColors.onPrimaryContainer.getArgb(dynamicScheme));
expect(colorScheme.primaryFixed.value, MaterialDynamicColors.primaryFixed.getArgb(dynamicScheme));
expect(colorScheme.primaryFixedDim.value, MaterialDynamicColors.primaryFixedDim.getArgb(dynamicScheme));
expect(colorScheme.onPrimaryFixed.value, MaterialDynamicColors.onPrimaryFixed.getArgb(dynamicScheme));
expect(colorScheme.onPrimaryFixedVariant.value, MaterialDynamicColors.onPrimaryFixedVariant.getArgb(dynamicScheme));
expect(colorScheme.secondary.value, MaterialDynamicColors.secondary.getArgb(dynamicScheme));
expect(colorScheme.onSecondary.value, MaterialDynamicColors.onSecondary.getArgb(dynamicScheme));
expect(colorScheme.secondaryContainer.value, MaterialDynamicColors.secondaryContainer.getArgb(dynamicScheme));
expect(colorScheme.onSecondaryContainer.value, MaterialDynamicColors.onSecondaryContainer.getArgb(dynamicScheme));
expect(colorScheme.secondaryFixed.value, MaterialDynamicColors.secondaryFixed.getArgb(dynamicScheme));
expect(colorScheme.secondaryFixedDim.value, MaterialDynamicColors.secondaryFixedDim.getArgb(dynamicScheme));
expect(colorScheme.onSecondaryFixed.value, MaterialDynamicColors.onSecondaryFixed.getArgb(dynamicScheme));
expect(colorScheme.onSecondaryFixedVariant.value, MaterialDynamicColors.onSecondaryFixedVariant.getArgb(dynamicScheme));
expect(colorScheme.tertiary.value, MaterialDynamicColors.tertiary.getArgb(dynamicScheme));
expect(colorScheme.onTertiary.value, MaterialDynamicColors.onTertiary.getArgb(dynamicScheme));
expect(colorScheme.tertiaryContainer.value, MaterialDynamicColors.tertiaryContainer.getArgb(dynamicScheme));
expect(colorScheme.onTertiaryContainer.value, MaterialDynamicColors.onTertiaryContainer.getArgb(dynamicScheme));
expect(colorScheme.tertiaryFixed.value, MaterialDynamicColors.tertiaryFixed.getArgb(dynamicScheme));
expect(colorScheme.tertiaryFixedDim.value, MaterialDynamicColors.tertiaryFixedDim.getArgb(dynamicScheme));
expect(colorScheme.onTertiaryFixed.value, MaterialDynamicColors.onTertiaryFixed.getArgb(dynamicScheme));
expect(colorScheme.onTertiaryFixedVariant.value, MaterialDynamicColors.onTertiaryFixedVariant.getArgb(dynamicScheme));
expect(colorScheme.error.value, MaterialDynamicColors.error.getArgb(dynamicScheme));
expect(colorScheme.onError.value, MaterialDynamicColors.onError.getArgb(dynamicScheme));
expect(colorScheme.errorContainer.value, MaterialDynamicColors.errorContainer.getArgb(dynamicScheme));
expect(colorScheme.onErrorContainer.value, MaterialDynamicColors.onErrorContainer.getArgb(dynamicScheme));
expect(colorScheme.background.value, MaterialDynamicColors.background.getArgb(dynamicScheme));
expect(colorScheme.onBackground.value, MaterialDynamicColors.onBackground.getArgb(dynamicScheme));
expect(colorScheme.surface.value, MaterialDynamicColors.surface.getArgb(dynamicScheme));
expect(colorScheme.surfaceDim.value, MaterialDynamicColors.surfaceDim.getArgb(dynamicScheme));
expect(colorScheme.surfaceBright.value, MaterialDynamicColors.surfaceBright.getArgb(dynamicScheme));
expect(colorScheme.surfaceContainerLowest.value, MaterialDynamicColors.surfaceContainerLowest.getArgb(dynamicScheme));
expect(colorScheme.surfaceContainerLow.value, MaterialDynamicColors.surfaceContainerLow.getArgb(dynamicScheme));
expect(colorScheme.surfaceContainer.value, MaterialDynamicColors.surfaceContainer.getArgb(dynamicScheme));
expect(colorScheme.surfaceContainerHigh.value, MaterialDynamicColors.surfaceContainerHigh.getArgb(dynamicScheme));
expect(colorScheme.surfaceContainerHighest.value, MaterialDynamicColors.surfaceContainerHighest.getArgb(dynamicScheme));
expect(colorScheme.onSurface.value, MaterialDynamicColors.onSurface.getArgb(dynamicScheme));
expect(colorScheme.surfaceVariant.value, MaterialDynamicColors.surfaceVariant.getArgb(dynamicScheme));
expect(colorScheme.onSurfaceVariant.value, MaterialDynamicColors.onSurfaceVariant.getArgb(dynamicScheme));
expect(colorScheme.outline.value, MaterialDynamicColors.outline.getArgb(dynamicScheme));
expect(colorScheme.outlineVariant.value, MaterialDynamicColors.outlineVariant.getArgb(dynamicScheme));
expect(colorScheme.shadow.value, MaterialDynamicColors.shadow.getArgb(dynamicScheme));
expect(colorScheme.scrim.value, MaterialDynamicColors.scrim.getArgb(dynamicScheme));
expect(colorScheme.inverseSurface.value, MaterialDynamicColors.inverseSurface.getArgb(dynamicScheme));
expect(colorScheme.onInverseSurface.value, MaterialDynamicColors.inverseOnSurface.getArgb(dynamicScheme));
expect(colorScheme.inversePrimary.value, MaterialDynamicColors.inversePrimary.getArgb(dynamicScheme));
}
for (final DynamicSchemeVariant schemeVariant in DynamicSchemeVariant.values) {
colorsMatchDynamicSchemeColors(schemeVariant, Brightness.light, 1.0); // High contrast
colorsMatchDynamicSchemeColors(schemeVariant, Brightness.dark, 1.0);
colorsMatchDynamicSchemeColors(schemeVariant, Brightness.light, 0.5); // Medium contrast
colorsMatchDynamicSchemeColors(schemeVariant, Brightness.dark, 0.5);
}
});
}
Future<void> _testFilledButtonColor(WidgetTester tester, ColorScheme scheme, Color expectation) async {