rydmike / flex_color_picker

A highly customizable Flutter color picker.
BSD 3-Clause "New" or "Revised" License
200 stars 42 forks source link

feature request: make OK button more intuitive #74

Open Piotr12 opened 10 months ago

Piotr12 commented 10 months ago

I have noticed some of my app users have difficulty noticing they need to confirm color selection by clicking the OK button. Seriously, some close the dialog and are surprised the color was not changed.

Question: Would it be ok to add a bool parameter in the ColorPickerActionButtons (updateOKButtonLikeCrazyToShowUsersWhatITDoes is the working title) that would modify the background color of the OK Button so it makes folks notice "here is what to click next" ?

If yes, I would be happy to make a PR with that, but before I start googling how to 1) modify, 2)test flutter packages locally I decided to ask not to get a "it is not welcome" response later.

PS. Font Color for the OK button shall be changed as well based on the grayscale representation of the color currently picked to avoid white font on almost-white background scenario. (https://support.ptc.com/help/mathcad/r9.0/en/index.html#page/PTC_Mathcad_Help/example_grayscale_and_color_in_images.html)

rydmike commented 10 months ago

Hi @Piotr12,

There are two options you can use to currently style the OK/Close buttons.

1. Wrap with desired button theme

You wrap it with a theme where the type of Text/Elevated/Outlined button you decide to use for "OK" has a more prominent style, and you can of course set labels to whatever you like.

This can look like this:

https://github.com/rydmike/flex_color_picker/assets/39990307/d08a8ddc-3848-4bb1-94f3-5a3ccb7bbd57

The above is a modified version of the default example in the repo

Code example ```dart import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/material.dart'; import 'demo/utils/app_scroll_behavior.dart'; void main() => runApp(const ColorPickerDemo()); class ColorPickerDemo extends StatefulWidget { const ColorPickerDemo({super.key}); @override State createState() => _ColorPickerDemoState(); } class _ColorPickerDemoState extends State { late ThemeMode themeMode; @override void initState() { super.initState(); themeMode = ThemeMode.light; } @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, scrollBehavior: AppScrollBehavior(), title: 'ColorPicker', theme: ThemeData(useMaterial3: true), darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark), themeMode: themeMode, home: ColorPickerPage( themeMode: (ThemeMode mode) { setState(() { themeMode = mode; }); }, ), ); } } class ColorPickerPage extends StatefulWidget { const ColorPickerPage({super.key, required this.themeMode}); final ValueChanged themeMode; @override State createState() => _ColorPickerPageState(); } class _ColorPickerPageState extends State { late Color screenPickerColor; // Color for picker shown in Card on the screen. late Color dialogPickerColor; // Color for picker in dialog using onChanged late Color dialogSelectColor; // Color for picker using color select dialog. late bool isDark; // Define some custom colors for the custom picker segment. // The 'guide' color values are from // https://material.io/design/color/the-color-system.html#color-theme-creation static const Color guidePrimary = Color(0xFF6200EE); static const Color guidePrimaryVariant = Color(0xFF3700B3); static const Color guideSecondary = Color(0xFF03DAC6); static const Color guideSecondaryVariant = Color(0xFF018786); static const Color guideError = Color(0xFFB00020); static const Color guideErrorDark = Color(0xFFCF6679); static const Color blueBlues = Color(0xFF174378); // Make a custom ColorSwatch to name map from the above custom colors. final Map, String> colorsNameMap = , String>{ ColorTools.createPrimarySwatch(guidePrimary): 'Guide Purple', ColorTools.createPrimarySwatch(guidePrimaryVariant): 'Guide Purple Variant', ColorTools.createAccentSwatch(guideSecondary): 'Guide Teal', ColorTools.createAccentSwatch(guideSecondaryVariant): 'Guide Teal Variant', ColorTools.createPrimarySwatch(guideError): 'Guide Error', ColorTools.createPrimarySwatch(guideErrorDark): 'Guide Error Dark', ColorTools.createPrimarySwatch(blueBlues): 'Blue blues', }; @override void initState() { screenPickerColor = Colors.blue; dialogPickerColor = Colors.red; dialogSelectColor = const Color(0xFFA239CA); isDark = false; super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( centerTitle: true, title: const Text('ColorPicker Demo'), ), body: ListView( padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), children: [ const SizedBox(height: 16), // Pick color in a dialog. ListTile( title: const Text('Click this color to modify it in a dialog. ' 'The color is modified while dialog is open, but returns ' 'to previous value if dialog is cancelled'), subtitle: Text( // ignore: lines_longer_than_80_chars '${ColorTools.materialNameAndCode(dialogPickerColor, colorSwatchNameMap: colorsNameMap)} ' 'aka ${ColorTools.nameThatColor(dialogPickerColor)}', ), trailing: Theme( data: Theme.of(context).copyWith( elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: Colors.pinkAccent, foregroundColor: Colors.white, padding: const EdgeInsets.all(20), elevation: 0, ), ), ), child: Builder(builder: (BuildContext context) { return ColorIndicator( width: 44, height: 44, borderRadius: 4, color: dialogPickerColor, onSelectFocus: false, onSelect: () async { // Store current color before we open the dialog. final Color colorBeforeDialog = dialogPickerColor; // Wait for the picker to close, if dialog was dismissed, // then restore the color we had before it was opened. if (!(await colorPickerDialog(context))) { setState(() { dialogPickerColor = colorBeforeDialog; }); } }, ); }), ), ), ListTile( title: const Text('Click to select a new color from a dialog ' 'that uses custom open/close animation. The color is only ' 'modified after dialog is closed with OK'), subtitle: Text( // ignore: lines_longer_than_80_chars '${ColorTools.materialNameAndCode(dialogSelectColor, colorSwatchNameMap: colorsNameMap)} ' 'aka ${ColorTools.nameThatColor(dialogSelectColor)}', ), trailing: Theme( data: Theme.of(context).copyWith( elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: Colors.pinkAccent, foregroundColor: Colors.white, padding: const EdgeInsets.all(20), textStyle: const TextStyle(fontSize: 20), elevation: 0, ), ), ), child: Builder(builder: (BuildContext context) { return ColorIndicator( width: 40, height: 40, borderRadius: 0, color: dialogSelectColor, elevation: 1, onSelectFocus: false, onSelect: () async { // Wait for the dialog to return color selection result. final Color newColor = await showColorPickerDialog( // The dialog needs a context, we pass it in. context, // We use the dialogSelectColor, as its starting color. dialogSelectColor, title: Text('ColorPicker', style: Theme.of(context).textTheme.titleLarge), width: 40, height: 40, spacing: 0, runSpacing: 0, borderRadius: 0, wheelDiameter: 165, enableOpacity: true, showColorCode: true, colorCodeHasColor: true, pickersEnabled: { ColorPickerType.wheel: true, }, copyPasteBehavior: const ColorPickerCopyPasteBehavior( copyButton: true, pasteButton: true, longPressMenu: true, ), actionButtons: const ColorPickerActionButtons( useRootNavigator: true, okButton: true, closeButton: true, dialogActionButtons: true, dialogCancelButtonType: ColorPickerActionButtonType.text, dialogOkButtonType: ColorPickerActionButtonType.elevated, dialogOkButtonLabel: 'SELECT', ), transitionBuilder: (BuildContext context, Animation a1, Animation a2, Widget widget) { final double curvedValue = Curves.easeInOutBack.transform(a1.value) - 1.0; return Transform( transform: Matrix4.translationValues( 0.0, curvedValue * 200, 0.0), child: Opacity( opacity: a1.value, child: widget, ), ); }, transitionDuration: const Duration(milliseconds: 400), constraints: const BoxConstraints( minHeight: 480, minWidth: 320, maxWidth: 320), ); // We update the dialogSelectColor, to the returned result // color. If the dialog was dismissed it actually returns // the color we started with. The extra update for that // below does not really matter, but if you want you can // check if they are equal and skip the update below. setState(() { dialogSelectColor = newColor; }); }); }), ), ), // Show the selected color. ListTile( title: const Text('Select color below to change this color'), subtitle: Text('${ColorTools.materialNameAndCode(screenPickerColor)} ' 'aka ${ColorTools.nameThatColor(screenPickerColor)}'), trailing: ColorIndicator( width: 44, height: 44, borderRadius: 22, color: screenPickerColor, ), ), // Show the color picker in sized box in a raised card. SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.all(6), child: Card( elevation: 2, child: ColorPicker( // Use the screenPickerColor as start color. color: screenPickerColor, // Update the screenPickerColor using the callback. onColorChanged: (Color color) => setState(() => screenPickerColor = color), width: 44, height: 44, borderRadius: 22, heading: Text( 'Select color', style: Theme.of(context).textTheme.headlineSmall, ), subheading: Text( 'Select color shade', style: Theme.of(context).textTheme.titleMedium, ), ), ), ), ), // Theme mode toggle SwitchListTile( title: const Text('Turn ON for dark mode'), subtitle: const Text('Turn OFF for light mode'), value: isDark, onChanged: (bool value) { setState(() { isDark = value; widget.themeMode(isDark ? ThemeMode.dark : ThemeMode.light); }); }, ) ], ), ); } Future colorPickerDialog(BuildContext context) async { return ColorPicker( color: dialogPickerColor, onColorChanged: (Color color) => setState(() => dialogPickerColor = color), width: 40, height: 40, borderRadius: 4, spacing: 5, runSpacing: 5, wheelDiameter: 155, heading: Text( 'Select color', style: Theme.of(context).textTheme.titleMedium, ), subheading: Text( 'Select color shade', style: Theme.of(context).textTheme.titleMedium, ), wheelSubheading: Text( 'Selected color and its shades', style: Theme.of(context).textTheme.titleMedium, ), showMaterialName: true, showColorName: true, showColorCode: true, copyPasteBehavior: const ColorPickerCopyPasteBehavior( longPressMenu: true, ), actionButtons: const ColorPickerActionButtons( useRootNavigator: false, dialogActionButtons: true, dialogCancelButtonType: ColorPickerActionButtonType.text, dialogOkButtonType: ColorPickerActionButtonType.elevated, dialogOkButtonLabel: 'PICK COLOR', ), materialNameTextStyle: Theme.of(context).textTheme.bodySmall, colorNameTextStyle: Theme.of(context).textTheme.bodySmall, colorCodeTextStyle: Theme.of(context).textTheme.bodyMedium, colorCodePrefixStyle: Theme.of(context).textTheme.bodySmall, selectedPickerTypeColor: Theme.of(context).colorScheme.primary, pickersEnabled: const { ColorPickerType.both: false, ColorPickerType.primary: true, ColorPickerType.accent: true, ColorPickerType.bw: false, ColorPickerType.custom: true, ColorPickerType.wheel: true, }, enableTonalPalette: true, // Enable tonal palette customColorSwatchesAndNames: colorsNameMap, ).showPickerDialog( context, actionsPadding: const EdgeInsets.all(16), constraints: const BoxConstraints(minHeight: 480, minWidth: 300, maxWidth: 320), ); } } ```
Custom OK 1 Custom OK 2 
Screenshot 2024-01-26 at 18 20 16 Screenshot 2024-01-26 at 18 20 45

2. Make your own dialog wrapper

You can make your own dialog wrapper of the ColorPicker and not use the built-in one at all. Doing so you can make any style dialog buttons you want. The built in was is based on AlertDialog, so it limits things a bit.

3. Do not use any bottom OK/Cancel dialog buttons

I kind of prefer the compact options where you just have close and select in the header.

Screenshot 2024-01-26 at 18 30 36

OK button that follows the currently selected color?

Upon reading your proposal closer, I'm beginning to suspect that you would like to see a feature flag that if set makes the dialog "OK" button color follow the currently selected color?

Then you can set its label to PICK, SELECT, CHOOSE, USE or whatever. Agreed then it also needs to adjust text contrast color while it does that. This would be like what the optional color value input/indicator does below:

https://github.com/rydmike/flex_color_picker/assets/39990307/d71456ad-2049-459c-b531-e8d20578568d

And check marks also do that when you select colors.

Yes this is doable, not that tricky even. It would however only work well visually when the OK button style is set to use ElevatedButton (like I did on above example). The default TextButton does not have a background color, nor does OutlinedButton. It would also work with the FilledButton, but there is no support for it in current version, I should of course add it as well.

Is this what you had in mind? Feel free to elaborate on the feature request.

I can certainly add this as a feature to next minor feature release. What should we call the property? okButtonUseSelectedColor? 😄

Piotr12 commented 10 months ago

thanks for detailed answer.

Upon reading your proposal closer, I'm beginning to suspect that you would like to see a feature flag that if set makes the dialog "OK" button color follow the currently selected color?

this is exactly what I look for and okButtonUseSelectedColor looks like a good name. But ... after thinking a bit more and taking into account your comment it will work only for elevatedButton it may make more sense to introduce new value for ColorPickerActionButtonType enum (no clue on good name for it :)) that will take care of that feature so its not a logical AND of 1) "new flag enabled" and 2) "right button style selected" to enable it. hope that makes sense, if not ... a bool flag will for sure do.

Link: https://pub.dev/documentation/flex_color_picker/latest/flex_color_picker/ColorPickerActionButtonType.html

rydmike commented 8 months ago

Sorry to say, but this colored "OK" button cannot be done within the currently used AlertDialog. simply because the OK and Cancel buttons are in the AlertDialog widget and not in ColorPicker widget. So I have no access to adjusting them after the Dialog has been created, so I cannot make OK button follow follow the selected color like the color indicator/entry field.

Best I can do in next release (v.3.4.0) is recommend using the "filled" button for OK as prominent one if so needed, and not having any cancel button (also new in 3.4.0 to not have a bottom cancel button when bottom dialog buttons are used), only close in upper corner and tapping outside dialog as close:

Screenshot 2024-03-03 at 21 28 19

It is possible to build this, but then I need to add own bottom OK / Cancel buttons in the Dialog and having them as an option that are used if you opt for the selected color following OK button. Doable, I might return to this in version 4.0.0. When I am doing a lot of other planned changes.

Keeping this feature request issue open as reminder.