diff --git a/examples/api/lib/material/color_scheme/color_scheme.0.dart b/examples/api/lib/material/color_scheme/color_scheme.0.dart index 78a6af4439..48c6b78248 100644 --- a/examples/api/lib/material/color_scheme/color_scheme.0.dart +++ b/examples/api/lib/material/color_scheme/color_scheme.0.dart @@ -20,8 +20,17 @@ class ColorSchemeExample extends StatefulWidget { class _ColorSchemeExampleState extends State { Color selectedColor = ColorSeed.baseColor.color; Brightness selectedBrightness = Brightness.light; + double selectedContrast = 0.0; static const List 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 { colorScheme: ColorScheme.fromSeed( seedColor: selectedColor, brightness: selectedBrightness, + contrastLevel: selectedContrast, ) ), - home: Builder( - builder: (BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('ColorScheme'), - actions: [ + home: Scaffold( + appBar: AppBar( + title: const Text('ColorScheme'), + actions: [ + IconButton( + icon: const Icon(Icons.settings), + onPressed: () { + showModalBottomSheet( + 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: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.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 createState() => _SettingsState(); +} + +class _SettingsState extends State { + 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: [ + Center(child: Text('Settings', style: Theme.of(context).textTheme.titleLarge)), Row( children: [ - 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.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: [ + const Text('Seed color: '), + ...List.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: [ + 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: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Row( - children: [ - 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.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, ), ), diff --git a/examples/api/test/material/color_scheme/color_scheme.0_test.dart b/examples/api/test/material/color_scheme/color_scheme.0_test.dart index 58e0e142ff..eff5bae0e6 100644 --- a/examples/api/test/material/color_scheme/color_scheme.0_test.dart +++ b/examples/api/test/material/color_scheme/color_scheme.0_test.dart @@ -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)); diff --git a/packages/flutter/lib/src/material/color_scheme.dart b/packages/flutter/lib/src/material/color_scheme.dart index 30914823f7..fdfbda3a6a 100644 --- a/packages/flutter/lib/src/material/color_scheme.dart +++ b/packages/flutter/lib/src/material/color_scheme.dart @@ -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 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), }; } } diff --git a/packages/flutter/test/material/color_scheme_test.dart b/packages/flutter/test/material/color_scheme_test.dart index b65ca9b6d7..7a1ea4b3aa 100644 --- a/packages/flutter/test/material/color_scheme_test.dart +++ b/packages/flutter/test/material/color_scheme_test.dart @@ -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 _testFilledButtonColor(WidgetTester tester, ColorScheme scheme, Color expectation) async {