Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature request: make OK button more intuitive #74

Open
Piotr12 opened this issue Jan 24, 2024 · 3 comments
Open

feature request: make OK button more intuitive #74

Piotr12 opened this issue Jan 24, 2024 · 3 comments
Assignees
Labels
discussion enhancement New feature or request
Milestone

Comments

@Piotr12
Copy link

Piotr12 commented Jan 24, 2024

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
Copy link
Owner

rydmike commented Jan 26, 2024

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:

Screen.Recording.2024-01-26.at.18.06.38.mov

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

Code example
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<ColorPickerDemo> createState() => _ColorPickerDemoState();
}

class _ColorPickerDemoState extends State<ColorPickerDemo> {
  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> themeMode;

  @override
  State<ColorPickerPage> createState() => _ColorPickerPageState();
}

class _ColorPickerPageState extends State<ColorPickerPage> {
  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<ColorSwatch<Object>, String> colorsNameMap =
      <ColorSwatch<Object>, 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: <Widget>[
          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, bool>{
                          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<double> a1,
                            Animation<double> 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<bool> 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, bool>{
        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:

Screen.Recording.2024-01-26.at.18.34.27.mov

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? 😄

@rydmike rydmike added the enhancement New feature or request label Jan 26, 2024
@rydmike rydmike added this to the 3.4.0 milestone Jan 26, 2024
@rydmike rydmike self-assigned this Jan 27, 2024
@rydmike rydmike moved this from To do to In progress in FlexColorPicker Jan 27, 2024
@Piotr12
Copy link
Author

Piotr12 commented Jan 29, 2024

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 rydmike modified the milestones: 3.4.0, 4.0.0 Mar 3, 2024
@rydmike
Copy link
Owner

rydmike commented Mar 3, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion enhancement New feature or request
Projects
Status: In progress
Development

No branches or pull requests

2 participants