From abf8298657cb9515d26ac531a85fc5ae799c74dd Mon Sep 17 00:00:00 2001 From: Eilidh Southren Date: Wed, 22 Mar 2023 14:33:47 +0000 Subject: [PATCH] Add image-based color selection to M3 demo (#1703) * Add ColorScheme.fromImageProvider selection method * method cleanup * cleanup * Move changes to experimental/ * Move changes from stable branch * update image descriptions * update image selection border * add mac network permissions * comment responses --- .../material_3_demo/lib/constants.dart | 20 ++ experimental/material_3_demo/lib/home.dart | 324 +++++++++++++++--- experimental/material_3_demo/lib/main.dart | 30 +- .../macos/Runner/DebugProfile.entitlements | 2 + .../macos/Runner/Release.entitlements | 2 + .../test/color_screen_test.dart | 4 +- .../test/elevation_screen_test.dart | 4 +- .../test/typography_screen_test.dart | 4 +- 8 files changed, 325 insertions(+), 65 deletions(-) diff --git a/experimental/material_3_demo/lib/constants.dart b/experimental/material_3_demo/lib/constants.dart index 943b0ca18..26c88c7d6 100644 --- a/experimental/material_3_demo/lib/constants.dart +++ b/experimental/material_3_demo/lib/constants.dart @@ -13,6 +13,13 @@ const double largeWidthBreakpoint = 1500; const double transitionLength = 500; +// Whether the user has chosen a theme color via a direct [ColorSeed] selection, +// or an image [ColorImageProvider]. +enum ColorSelectionMethod { + colorSeed, + image, +} + enum ColorSeed { baseColor('M3 Baseline', Color(0xff6750a4)), indigo('Indigo', Colors.indigo), @@ -29,6 +36,19 @@ enum ColorSeed { final Color color; } +enum ColorImageProvider { + leaves('Leaves', 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_1.png'), + peonies('Peonies', 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_2.png'), + bubbles('Bubbles', 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_3.png'), + seaweed('Seaweed', 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_4.png'), + seagrapes('Sea Grapes', 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_5.png'), + petals('Petals', 'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_6.png'); + + const ColorImageProvider(this.label, this.url); + final String label; + final String url; +} + enum ScreenSelected { component(0), color(1), diff --git a/experimental/material_3_demo/lib/home.dart b/experimental/material_3_demo/lib/home.dart index b50e6573d..cf24b657c 100644 --- a/experimental/material_3_demo/lib/home.dart +++ b/experimental/material_3_demo/lib/home.dart @@ -19,14 +19,21 @@ class Home extends StatefulWidget { required this.handleBrightnessChange, required this.handleMaterialVersionChange, required this.handleColorSelect, + required this.handleImageSelect, + required this.colorSelectionMethod, + required this.imageSelected, }); final bool useLightMode; final bool useMaterial3; final ColorSeed colorSelected; + final ColorImageProvider imageSelected; + final ColorSelectionMethod colorSelectionMethod; + final void Function(bool useLightMode) handleBrightnessChange; final void Function() handleMaterialVersionChange; final void Function(int value) handleColorSelect; + final void Function(int value) handleImageSelect; @override State createState() => _HomeState(); @@ -146,66 +153,18 @@ class _HomeState extends State with SingleTickerProviderStateMixin { _ColorSeedButton( handleColorSelect: widget.handleColorSelect, colorSelected: widget.colorSelected, + colorSelectionMethod: widget.colorSelectionMethod, ), + _ColorImageButton( + handleImageSelect: widget.handleImageSelect, + imageSelected: widget.imageSelected, + colorSelectionMethod: widget.colorSelectionMethod, + ) ] : [Container()], ); } - Widget _expandedTrailingActions() => Container( - constraints: const BoxConstraints.tightFor(width: 250), - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - const Text('Brightness'), - Expanded(child: Container()), - Switch( - value: widget.useLightMode, - onChanged: (value) { - widget.handleBrightnessChange(value); - }) - ], - ), - Row( - children: [ - widget.useMaterial3 - ? const Text('Material 3') - : const Text('Material 2'), - Expanded(child: Container()), - Switch( - value: widget.useMaterial3, - onChanged: (_) { - widget.handleMaterialVersionChange(); - }) - ], - ), - const Divider(), - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200.0), - child: GridView.count( - crossAxisCount: 3, - children: List.generate( - ColorSeed.values.length, - (i) => IconButton( - icon: const Icon(Icons.radio_button_unchecked), - color: ColorSeed.values[i].color, - isSelected: widget.colorSelected.color == - ColorSeed.values[i].color, - selectedIcon: const Icon(Icons.circle), - onPressed: () { - widget.handleColorSelect(i); - }, - )), - ), - ), - ], - ), - ); - Widget _trailingActions() => Column( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -225,6 +184,14 @@ class _HomeState extends State with SingleTickerProviderStateMixin { child: _ColorSeedButton( handleColorSelect: widget.handleColorSelect, colorSelected: widget.colorSelected, + colorSelectionMethod: widget.colorSelectionMethod, + ), + ), + Flexible( + child: _ColorImageButton( + handleImageSelect: widget.handleImageSelect, + imageSelected: widget.imageSelected, + colorSelectionMethod: widget.colorSelectionMethod, ), ), ], @@ -256,7 +223,18 @@ class _HomeState extends State with SingleTickerProviderStateMixin { child: Padding( padding: const EdgeInsets.only(bottom: 20), child: showLargeSizeLayout - ? _expandedTrailingActions() + ? _ExpandedTrailingActions( + useLightMode: widget.useLightMode, + handleBrightnessChange: widget.handleBrightnessChange, + useMaterial3: widget.useMaterial3, + handleMaterialVersionChange: + widget.handleMaterialVersionChange, + handleImageSelect: widget.handleImageSelect, + handleColorSelect: widget.handleColorSelect, + colorSelectionMethod: widget.colorSelectionMethod, + imageSelected: widget.imageSelected, + colorSelected: widget.colorSelected, + ) : _trailingActions(), ), ), @@ -331,10 +309,12 @@ class _ColorSeedButton extends StatelessWidget { const _ColorSeedButton({ required this.handleColorSelect, required this.colorSelected, + required this.colorSelectionMethod, }); final void Function(int) handleColorSelect; final ColorSeed colorSelected; + final ColorSelectionMethod colorSelectionMethod; @override Widget build(BuildContext context) { @@ -351,13 +331,15 @@ class _ColorSeedButton extends StatelessWidget { return PopupMenuItem( value: index, - enabled: currentColor != colorSelected, + enabled: currentColor != colorSelected || + colorSelectionMethod != ColorSelectionMethod.colorSeed, child: Wrap( children: [ Padding( padding: const EdgeInsets.only(left: 10), child: Icon( - currentColor == colorSelected + currentColor == colorSelected && + colorSelectionMethod != ColorSelectionMethod.image ? Icons.color_lens : Icons.color_lens_outlined, color: currentColor.color, @@ -377,6 +359,234 @@ class _ColorSeedButton extends StatelessWidget { } } +class _ColorImageButton extends StatelessWidget { + const _ColorImageButton({ + required this.handleImageSelect, + required this.imageSelected, + required this.colorSelectionMethod, + }); + + final void Function(int) handleImageSelect; + final ColorImageProvider imageSelected; + final ColorSelectionMethod colorSelectionMethod; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: Icon( + Icons.image_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + tooltip: 'Select a color extraction image', + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + itemBuilder: (context) { + return List.generate(ColorImageProvider.values.length, (index) { + ColorImageProvider currentImageProvider = + ColorImageProvider.values[index]; + + return PopupMenuItem( + value: index, + enabled: currentImageProvider != imageSelected || + colorSelectionMethod != ColorSelectionMethod.image, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 48), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image( + image: NetworkImage( + ColorImageProvider.values[index].url), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 20), + child: Text(currentImageProvider.label), + ), + ], + ), + ); + }); + }, + onSelected: handleImageSelect, + ); + } +} + +class _ExpandedTrailingActions extends StatelessWidget { + const _ExpandedTrailingActions({ + required this.useLightMode, + required this.handleBrightnessChange, + required this.useMaterial3, + required this.handleMaterialVersionChange, + required this.handleColorSelect, + required this.handleImageSelect, + required this.imageSelected, + required this.colorSelected, + required this.colorSelectionMethod, + }); + + final void Function(bool) handleBrightnessChange; + final void Function() handleMaterialVersionChange; + final void Function(int) handleImageSelect; + final void Function(int) handleColorSelect; + + final bool useLightMode; + final bool useMaterial3; + + final ColorImageProvider imageSelected; + final ColorSeed colorSelected; + final ColorSelectionMethod colorSelectionMethod; + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + final trailingActionsBody = Container( + constraints: const BoxConstraints.tightFor(width: 250), + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + const Text('Brightness'), + Expanded(child: Container()), + Switch( + value: useLightMode, + onChanged: (value) { + handleBrightnessChange(value); + }) + ], + ), + Row( + children: [ + useMaterial3 + ? const Text('Material 3') + : const Text('Material 2'), + Expanded(child: Container()), + Switch( + value: useMaterial3, + onChanged: (_) { + handleMaterialVersionChange(); + }) + ], + ), + const Divider(), + _ExpandedColorSeedAction( + handleColorSelect: handleColorSelect, + colorSelected: colorSelected, + colorSelectionMethod: colorSelectionMethod, + ), + const Divider(), + _ExpandedImageColorAction( + handleImageSelect: handleImageSelect, + imageSelected: imageSelected, + colorSelectionMethod: colorSelectionMethod, + ), + ], + ), + ); + return screenHeight > 740 + ? trailingActionsBody + : SingleChildScrollView(child: trailingActionsBody); + } +} + +class _ExpandedColorSeedAction extends StatelessWidget { + const _ExpandedColorSeedAction({ + required this.handleColorSelect, + required this.colorSelected, + required this.colorSelectionMethod, + }); + + final void Function(int) handleColorSelect; + final ColorSeed colorSelected; + final ColorSelectionMethod colorSelectionMethod; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200.0), + child: GridView.count( + crossAxisCount: 3, + children: List.generate( + ColorSeed.values.length, + (i) => IconButton( + icon: const Icon(Icons.radio_button_unchecked), + color: ColorSeed.values[i].color, + isSelected: colorSelected.color == ColorSeed.values[i].color && + colorSelectionMethod == ColorSelectionMethod.colorSeed, + selectedIcon: const Icon(Icons.circle), + onPressed: () { + handleColorSelect(i); + }, + ), + ), + ), + ); + } +} + +class _ExpandedImageColorAction extends StatelessWidget { + const _ExpandedImageColorAction({ + required this.handleImageSelect, + required this.imageSelected, + required this.colorSelectionMethod, + }); + + final void Function(int) handleImageSelect; + final ColorImageProvider imageSelected; + final ColorSelectionMethod colorSelectionMethod; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 150.0), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: GridView.count( + crossAxisCount: 3, + children: List.generate( + ColorImageProvider.values.length, + (i) => InkWell( + borderRadius: BorderRadius.circular(4.0), + onTap: () => handleImageSelect(i), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + borderRadius: BorderRadius.circular(4.0), + elevation: imageSelected == ColorImageProvider.values[i] && + colorSelectionMethod == ColorSelectionMethod.image + ? 3 + : 0, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(4.0), + child: Image( + image: NetworkImage(ColorImageProvider.values[i].url), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} + class NavigationTransition extends StatefulWidget { const NavigationTransition( {super.key, diff --git a/experimental/material_3_demo/lib/main.dart b/experimental/material_3_demo/lib/main.dart index 262201921..5e647d1e0 100644 --- a/experimental/material_3_demo/lib/main.dart +++ b/experimental/material_3_demo/lib/main.dart @@ -25,6 +25,9 @@ class _AppState extends State { bool useMaterial3 = true; ThemeMode themeMode = ThemeMode.system; ColorSeed colorSelected = ColorSeed.baseColor; + ColorImageProvider imageSelected = ColorImageProvider.leaves; + ColorScheme? imageColorScheme = const ColorScheme.light(); + ColorSelectionMethod colorSelectionMethod = ColorSelectionMethod.colorSeed; bool get useLightMode { switch (themeMode) { @@ -52,10 +55,23 @@ class _AppState extends State { void handleColorSelect(int value) { setState(() { + colorSelectionMethod = ColorSelectionMethod.colorSeed; colorSelected = ColorSeed.values[value]; }); } + void handleImageSelect(int value) { + final String url = ColorImageProvider.values[value].url; + ColorScheme.fromImageProvider(provider: NetworkImage(url)) + .then((newScheme) { + setState(() { + colorSelectionMethod = ColorSelectionMethod.image; + imageSelected = ColorImageProvider.values[value]; + imageColorScheme = newScheme; + }); + }); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -63,12 +79,19 @@ class _AppState extends State { title: 'Material 3', themeMode: themeMode, theme: ThemeData( - colorSchemeSeed: colorSelected.color, + colorSchemeSeed: colorSelectionMethod == ColorSelectionMethod.colorSeed + ? colorSelected.color + : null, + colorScheme: colorSelectionMethod == ColorSelectionMethod.image + ? imageColorScheme + : null, useMaterial3: useMaterial3, brightness: Brightness.light, ), darkTheme: ThemeData( - colorSchemeSeed: colorSelected.color, + colorSchemeSeed: colorSelectionMethod == ColorSelectionMethod.colorSeed + ? colorSelected.color + : imageColorScheme!.primary, useMaterial3: useMaterial3, brightness: Brightness.dark, ), @@ -76,9 +99,12 @@ class _AppState extends State { useLightMode: useLightMode, useMaterial3: useMaterial3, colorSelected: colorSelected, + imageSelected: imageSelected, handleBrightnessChange: handleBrightnessChange, handleMaterialVersionChange: handleMaterialVersionChange, handleColorSelect: handleColorSelect, + handleImageSelect: handleImageSelect, + colorSelectionMethod: colorSelectionMethod, ), ); } diff --git a/experimental/material_3_demo/macos/Runner/DebugProfile.entitlements b/experimental/material_3_demo/macos/Runner/DebugProfile.entitlements index dddb8a30c..08c3ab17c 100644 --- a/experimental/material_3_demo/macos/Runner/DebugProfile.entitlements +++ b/experimental/material_3_demo/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/experimental/material_3_demo/macos/Runner/Release.entitlements b/experimental/material_3_demo/macos/Runner/Release.entitlements index 852fa1a47..ee95ab7e5 100644 --- a/experimental/material_3_demo/macos/Runner/Release.entitlements +++ b/experimental/material_3_demo/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client + diff --git a/experimental/material_3_demo/test/color_screen_test.dart b/experimental/material_3_demo/test/color_screen_test.dart index 64efe5884..578bc02e9 100644 --- a/experimental/material_3_demo/test/color_screen_test.dart +++ b/experimental/material_3_demo/test/color_screen_test.dart @@ -65,8 +65,8 @@ void main() { }); testWidgets('Color screen shows correct content', (tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold(body: Row(children: const [ColorPalettesScreen()])), + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: Row(children: [ColorPalettesScreen()])), )); expect(find.text('Light ColorScheme'), findsOneWidget); expect(find.text('Dark ColorScheme'), findsOneWidget); diff --git a/experimental/material_3_demo/test/elevation_screen_test.dart b/experimental/material_3_demo/test/elevation_screen_test.dart index 95d886e5f..e71f574b5 100644 --- a/experimental/material_3_demo/test/elevation_screen_test.dart +++ b/experimental/material_3_demo/test/elevation_screen_test.dart @@ -57,8 +57,8 @@ void main() { }); testWidgets('Surface Tones screen shows correct content', (tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold(body: Row(children: const [ElevationScreen()])), + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: Row(children: [ElevationScreen()])), )); expect(find.text('Surface Tint Color Only'), findsOneWidget); expect(find.text('Surface Tint Color and Shadow Color'), findsOneWidget); diff --git a/experimental/material_3_demo/test/typography_screen_test.dart b/experimental/material_3_demo/test/typography_screen_test.dart index fff11f9ec..456747eb4 100644 --- a/experimental/material_3_demo/test/typography_screen_test.dart +++ b/experimental/material_3_demo/test/typography_screen_test.dart @@ -57,8 +57,8 @@ void main() { }); testWidgets('Typography screen shows correct content', (tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold(body: Row(children: const [TypographyScreen()])), + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: Row(children: [TypographyScreen()])), )); expect(find.text('Display Large'), findsOneWidget); expect(find.text('Display Medium'), findsOneWidget);