mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 22:09:06 +00:00
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
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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<Home> createState() => _HomeState();
|
||||
@@ -146,66 +153,18 @@ class _HomeState extends State<Home> 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<Home> 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<Home> 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,
|
||||
|
||||
@@ -25,6 +25,9 @@ class _AppState extends State<App> {
|
||||
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<App> {
|
||||
|
||||
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<App> {
|
||||
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<App> {
|
||||
useLightMode: useLightMode,
|
||||
useMaterial3: useMaterial3,
|
||||
colorSelected: colorSelected,
|
||||
imageSelected: imageSelected,
|
||||
handleBrightnessChange: handleBrightnessChange,
|
||||
handleMaterialVersionChange: handleMaterialVersionChange,
|
||||
handleColorSelect: handleColorSelect,
|
||||
handleImageSelect: handleImageSelect,
|
||||
colorSelectionMethod: colorSelectionMethod,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,5 +8,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -4,5 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user