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
pull/1705/head
Eilidh Southren 2 years ago committed by GitHub
parent 481c2e3d1d
commit abf8298657
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,6 +13,13 @@ const double largeWidthBreakpoint = 1500;
const double transitionLength = 500; 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 { enum ColorSeed {
baseColor('M3 Baseline', Color(0xff6750a4)), baseColor('M3 Baseline', Color(0xff6750a4)),
indigo('Indigo', Colors.indigo), indigo('Indigo', Colors.indigo),
@ -29,6 +36,19 @@ enum ColorSeed {
final Color color; 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 { enum ScreenSelected {
component(0), component(0),
color(1), color(1),

@ -19,14 +19,21 @@ class Home extends StatefulWidget {
required this.handleBrightnessChange, required this.handleBrightnessChange,
required this.handleMaterialVersionChange, required this.handleMaterialVersionChange,
required this.handleColorSelect, required this.handleColorSelect,
required this.handleImageSelect,
required this.colorSelectionMethod,
required this.imageSelected,
}); });
final bool useLightMode; final bool useLightMode;
final bool useMaterial3; final bool useMaterial3;
final ColorSeed colorSelected; final ColorSeed colorSelected;
final ColorImageProvider imageSelected;
final ColorSelectionMethod colorSelectionMethod;
final void Function(bool useLightMode) handleBrightnessChange; final void Function(bool useLightMode) handleBrightnessChange;
final void Function() handleMaterialVersionChange; final void Function() handleMaterialVersionChange;
final void Function(int value) handleColorSelect; final void Function(int value) handleColorSelect;
final void Function(int value) handleImageSelect;
@override @override
State<Home> createState() => _HomeState(); State<Home> createState() => _HomeState();
@ -146,66 +153,18 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
_ColorSeedButton( _ColorSeedButton(
handleColorSelect: widget.handleColorSelect, handleColorSelect: widget.handleColorSelect,
colorSelected: widget.colorSelected, colorSelected: widget.colorSelected,
colorSelectionMethod: widget.colorSelectionMethod,
), ),
_ColorImageButton(
handleImageSelect: widget.handleImageSelect,
imageSelected: widget.imageSelected,
colorSelectionMethod: widget.colorSelectionMethod,
)
] ]
: [Container()], : [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( Widget _trailingActions() => Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -225,6 +184,14 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
child: _ColorSeedButton( child: _ColorSeedButton(
handleColorSelect: widget.handleColorSelect, handleColorSelect: widget.handleColorSelect,
colorSelected: widget.colorSelected, 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<Home> with SingleTickerProviderStateMixin {
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.only(bottom: 20),
child: showLargeSizeLayout 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(), : _trailingActions(),
), ),
), ),
@ -331,10 +309,12 @@ class _ColorSeedButton extends StatelessWidget {
const _ColorSeedButton({ const _ColorSeedButton({
required this.handleColorSelect, required this.handleColorSelect,
required this.colorSelected, required this.colorSelected,
required this.colorSelectionMethod,
}); });
final void Function(int) handleColorSelect; final void Function(int) handleColorSelect;
final ColorSeed colorSelected; final ColorSeed colorSelected;
final ColorSelectionMethod colorSelectionMethod;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -351,13 +331,15 @@ class _ColorSeedButton extends StatelessWidget {
return PopupMenuItem( return PopupMenuItem(
value: index, value: index,
enabled: currentColor != colorSelected, enabled: currentColor != colorSelected ||
colorSelectionMethod != ColorSelectionMethod.colorSeed,
child: Wrap( child: Wrap(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 10), padding: const EdgeInsets.only(left: 10),
child: Icon( child: Icon(
currentColor == colorSelected currentColor == colorSelected &&
colorSelectionMethod != ColorSelectionMethod.image
? Icons.color_lens ? Icons.color_lens
: Icons.color_lens_outlined, : Icons.color_lens_outlined,
color: currentColor.color, 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 { class NavigationTransition extends StatefulWidget {
const NavigationTransition( const NavigationTransition(
{super.key, {super.key,

@ -25,6 +25,9 @@ class _AppState extends State<App> {
bool useMaterial3 = true; bool useMaterial3 = true;
ThemeMode themeMode = ThemeMode.system; ThemeMode themeMode = ThemeMode.system;
ColorSeed colorSelected = ColorSeed.baseColor; ColorSeed colorSelected = ColorSeed.baseColor;
ColorImageProvider imageSelected = ColorImageProvider.leaves;
ColorScheme? imageColorScheme = const ColorScheme.light();
ColorSelectionMethod colorSelectionMethod = ColorSelectionMethod.colorSeed;
bool get useLightMode { bool get useLightMode {
switch (themeMode) { switch (themeMode) {
@ -52,10 +55,23 @@ class _AppState extends State<App> {
void handleColorSelect(int value) { void handleColorSelect(int value) {
setState(() { setState(() {
colorSelectionMethod = ColorSelectionMethod.colorSeed;
colorSelected = ColorSeed.values[value]; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
@ -63,12 +79,19 @@ class _AppState extends State<App> {
title: 'Material 3', title: 'Material 3',
themeMode: themeMode, themeMode: themeMode,
theme: ThemeData( theme: ThemeData(
colorSchemeSeed: colorSelected.color, colorSchemeSeed: colorSelectionMethod == ColorSelectionMethod.colorSeed
? colorSelected.color
: null,
colorScheme: colorSelectionMethod == ColorSelectionMethod.image
? imageColorScheme
: null,
useMaterial3: useMaterial3, useMaterial3: useMaterial3,
brightness: Brightness.light, brightness: Brightness.light,
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
colorSchemeSeed: colorSelected.color, colorSchemeSeed: colorSelectionMethod == ColorSelectionMethod.colorSeed
? colorSelected.color
: imageColorScheme!.primary,
useMaterial3: useMaterial3, useMaterial3: useMaterial3,
brightness: Brightness.dark, brightness: Brightness.dark,
), ),
@ -76,9 +99,12 @@ class _AppState extends State<App> {
useLightMode: useLightMode, useLightMode: useLightMode,
useMaterial3: useMaterial3, useMaterial3: useMaterial3,
colorSelected: colorSelected, colorSelected: colorSelected,
imageSelected: imageSelected,
handleBrightnessChange: handleBrightnessChange, handleBrightnessChange: handleBrightnessChange,
handleMaterialVersionChange: handleMaterialVersionChange, handleMaterialVersionChange: handleMaterialVersionChange,
handleColorSelect: handleColorSelect, handleColorSelect: handleColorSelect,
handleImageSelect: handleImageSelect,
colorSelectionMethod: colorSelectionMethod,
), ),
); );
} }

@ -8,5 +8,7 @@
<true/> <true/>
<key>com.apple.security.network.server</key> <key>com.apple.security.network.server</key>
<true/> <true/>
<key>com.apple.security.network.client</key>
<true/>
</dict> </dict>
</plist> </plist>

@ -4,5 +4,7 @@
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.network.client</key>
<true/>
</dict> </dict>
</plist> </plist>

@ -65,8 +65,8 @@ void main() {
}); });
testWidgets('Color screen shows correct content', (tester) async { testWidgets('Color screen shows correct content', (tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: Row(children: const [ColorPalettesScreen()])), home: Scaffold(body: Row(children: [ColorPalettesScreen()])),
)); ));
expect(find.text('Light ColorScheme'), findsOneWidget); expect(find.text('Light ColorScheme'), findsOneWidget);
expect(find.text('Dark ColorScheme'), findsOneWidget); expect(find.text('Dark ColorScheme'), findsOneWidget);

@ -57,8 +57,8 @@ void main() {
}); });
testWidgets('Surface Tones screen shows correct content', (tester) async { testWidgets('Surface Tones screen shows correct content', (tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: Row(children: const [ElevationScreen()])), home: Scaffold(body: Row(children: [ElevationScreen()])),
)); ));
expect(find.text('Surface Tint Color Only'), findsOneWidget); expect(find.text('Surface Tint Color Only'), findsOneWidget);
expect(find.text('Surface Tint Color and Shadow Color'), findsOneWidget); expect(find.text('Surface Tint Color and Shadow Color'), findsOneWidget);

@ -57,8 +57,8 @@ void main() {
}); });
testWidgets('Typography screen shows correct content', (tester) async { testWidgets('Typography screen shows correct content', (tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: Row(children: const [TypographyScreen()])), home: Scaffold(body: Row(children: [TypographyScreen()])),
)); ));
expect(find.text('Display Large'), findsOneWidget); expect(find.text('Display Large'), findsOneWidget);
expect(find.text('Display Medium'), findsOneWidget); expect(find.text('Display Medium'), findsOneWidget);

Loading…
Cancel
Save