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

Improvements in the implementation of dynamic colors #579

Open
IlluminatiWave opened this issue Apr 10, 2024 · 0 comments
Open

Improvements in the implementation of dynamic colors #579

IlluminatiWave opened this issue Apr 10, 2024 · 0 comments
Labels
enhancement New feature or request p: dynamic_color

Comments

@IlluminatiWave
Copy link

Package

dynamic_color

Description

Is your feature request related to a problem? Please describe.

The color rendering in the plugin for non-Android operating systems needs significant improvements. Currently, the color palette is limited and the documentation isn't clear enough.

Describe the solution you'd like

  1. Enhanced support for color palette on other operating systems:
    I request improved color palette support for operating systems other than Android, allowing for greater customization.

  2. Improved documentation and example files:
    More detailed documentation and better designed examples are needed to facilitate understanding and use of the plugin.

  3. Real-time update of the color palette:
    I wish the plugin could update the color palette in real-time, similar to how Flutter is able to dynamically switch between light/dark themes depending on the current device theme. It would be ideal to integrate this functionality as part of native material.dart.

Additional context

Consider including functionality such as ColorScheme.fromImageProvider or DynamicColorPlugin.getAccentColor for more accurate color rendering and integration with dynamic system themes, such as those that Flutter is able to detect (light/dark), for a more consistent experience.


Describe the solution you'd like
While Windows currently uses a SystemAccentColorLight/Dark 1/2/3 system, it is not entirely useful for the material theme system.

Currently, Flutter doesn't dynamically adapt to Windows theming, which limits its ability to reflect changes in accent color and light/dark themes. I propose to update the Flutter plugin to allow dynamic response to these changes in the first instance, focusing on improving the functionality of DynamicColorPlugin.getAccentColor, which currently works statically. I have implemented code that improves this functionality, making it dynamic.

void updateColor() async {
  Duration delay = const Duration(/*seconds: 1*/);
  isDynamicColor = true;
  while (isDynamicColor) {
    final newColor = await DynamicColorPlugin.getAccentColor();
    if (newColor != currentColor) {
      setState(() {
        currentColor = newColor;
      });
    }
    await Future.delayed(delay);
  }
}
Form of use
class _MyAppState extends State<MyApp> {
  Color? currentColor;
  bool isDynamicColor = true; 

  @override
  void initState() {
    super.initState();
    updateColor();
  }

  Widget build(BuildContext context) {
    final color = currentColor ?? Colors.blue;
    return MaterialApp(
      themeMode: ThemeMode.system,
      theme: ThemeData.light().copyWith(
        colorScheme: ColorScheme.fromSeed(seedColor: color, brightness: Brightness.light),
      ),
      darkTheme: ThemeData.dark().copyWith(
        colorScheme: ColorScheme.fromSeed(seedColor: color, brightness: Brightness.dark),
      ),
      debugShowCheckedModeBanner: false,
      home: const MyHomePage(),
    );
  }
}
/*
Then you can use the theme colors automatically, it is compatible with Material3.
Dynamic color is disabled when setting isDynamicColor = false;
and activating it by calling the updateColor(); function.
*/
Real Code (Windows)
import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';

void main() {
  runApp(const DynamicColorExample());
}

class DynamicColorExample extends StatefulWidget {
  const DynamicColorExample({super.key});
  @override
  // ignore: library_private_types_in_public_api
  _DynamicColorExampleState createState() => _DynamicColorExampleState();
}

class _DynamicColorExampleState extends State<DynamicColorExample> {
  Color? currentColor;
  bool isDynamicColor = true;

  @override
  void initState() {
    super.initState();
    updateColor();
  }

  void updateColor() async {
    Duration delay = const Duration(/*seconds: 1*/);
    isDynamicColor = true;
    while (isDynamicColor) {
      final newColor = await DynamicColorPlugin.getAccentColor();
      if (newColor != currentColor) {
        setState(() {
          currentColor = newColor;
        });
      }
      await Future.delayed(delay);
    }
  }

  @override
  Widget build(BuildContext context) {
    final color = currentColor ?? Colors.blue;
    return MaterialApp(
      themeMode: ThemeMode.system,
      theme: ThemeData.light().copyWith(
        colorScheme: ColorScheme.fromSeed(
            seedColor: color, brightness: Brightness.light),
      ),
      darkTheme: ThemeData.dark().copyWith(
        colorScheme:
            ColorScheme.fromSeed(seedColor: color, brightness: Brightness.dark),
      ),
      debugShowCheckedModeBanner: false,
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Testapp'),
        backgroundColor: Theme.of(context).colorScheme.primaryContainer,
      ),
      body: const Center(
        child: Text('Hello World!'),
      ),
      backgroundColor: Theme.of(context).colorScheme.background,
      drawer: Drawer(
        child: ListView(
          children: [
            DrawerHeader(
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.secondaryContainer,
              ),
              child: const Text('Drawer'),
            ),
            ListTile(
              leading: const Icon(Icons.home),
              title: const Text('Home'),
              onTap: () {},
            ),
            ListTile(
              leading: const Icon(Icons.settings),
              title: const Text('Settings'),
              onTap: () {},
            ),
          ],
        ),
      ),
    );
  }
}
AccentColor.mp4

With this change, the accent color in Flutter now dynamically adapts to the system accent color in Windows.

In addition, the code provided has been tested and works correctly on Windows. It is worth noting that the dynamic theming has not been tested on other platforms such as MacOS or Linux (it should work). However, a static blue theme is generated by default for the web platform where dynamic theming isn't supported.

Furthermore, this implementation could potentially serve as the default color scheme for desktop platforms (material.dart).

Adaptive/Responsive Color Scheme.

Android's Material You and Material Design include adaptive theming based on the wallpaper. Meanwhile, Windows mainly relies on accent color changes based on the wallpaper. However, achieving similar results on other platforms is complex due to factors such as the macOS system or the way wallpapers are stored in various Linux desktop environments. The code provided, while primarily focused on Windows, uses a modified version of DynamicColorExample that uses ColorScheme.fromImageProvider to obtain a color palette from an image. While not identical to native Android theming, it achieves a similar result by dynamically adapting the theme based on the wallpaper.

That is why, in order to achieve similar performance, I have focused on 3 goals:

  1. Get the wallpaper path (only works on Windows at the moment): To adapt the theme according to the wallpaper, it is crucial to get the path of the wallpaper image file in use.

  2. Listen for possible changes in the wallpaper: The ability to detect changes in the wallpaper is essential to dynamically update the color scheme in response to changes in the system wallpaper.

  3. Set the theme whenever a change in the wallpaper is detected: Once a change in the wallpaper is detected, it is necessary to automatically update the application theme to reflect the new wallpaper colors.


Goal 1: Obtain the path to the wallpaper (only works on Windows at the moment).

The following code is responsible for obtaining an image file that represents the wallpaper, depending on the operating system in use. Although it works effectively in Windows, it has conditions of use for when it is possible to find a way to obtain the image of the wallpaper in other operating systems.

import 'dart:io';

File getWallpaperFile() {
  String wallpaperPath = '';
  if (Platform.isWindows) {
    wallpaperPath =
        '${Platform.environment['APPDATA']}\\Microsoft\\Windows\\Themes\\transcodedWallpaper';
  } else if (Platform.isMacOS) {
    /*wallpaperPath =
        '${Platform.environment['HOME']}/Library/Application Support/Dock/desktoppicture.db';*/
    // Additional research is needed to extract the real-time image on MacOS
  } else if (Platform.isLinux) {
    // Code to get the background on Linux platform (Pending implementation)
  } else if (Platform.isAndroid) {
    // Code to get the background on Android platform (Pending implementation)
  } else if (Platform.isIOS) {
    // Code to get the background on iOS platform (Pending implementation)
  } else if (Platform.isFuchsia) {
    // Code to get the background on Fuchsia platform (Pending implementation)
  }

  if (wallpaperPath.isNotEmpty) {
    return File(wallpaperPath);
  } else {
    return File('');
  }
}

As mentioned, the code currently only works on Windows. Additional research is needed to determine how to get the real-time wallpaper image on macOS, as well as implement the functionality for other platforms.

Goal 2: Listen for possible changes in the wallpaper.

The following function, wallpaperwatcher, is in charge of monitoring a file representing the wallpaper. This function takes as parameter a File object, which is the same file obtained by the getWallpaperFile() function.

import 'dart:io';

Future<void> wallpaperwatcher(File file) async {
  DateTime lastModified = file.statSync().modified;
  Duration delay = const Duration(milliseconds: 100);

  while (true) {
    final currentModified = file.statSync().modified;
    if (currentModified != lastModified) {
      lastModified = currentModified;
      while (true) {
        await Future.delayed(delay); // Prevents system overload if the file doesn't change
        try {
          RandomAccessFile raf = await file.open(mode: FileMode.read);
          await raf.close(); // Avoid Windows background file usage problems
          break;
        } catch (e) {
          // In case of error detection, create a console.log(e);
        }
      }

      await (widget.imageKey.currentWidget as Image).image.evict();

      // Update the desktop wallpaper usage with a new widget Image
      ImageProvider<Object>? result;
      if (forceLoad) {
        result = MemoryImage(imagePath.readAsBytesSync());
      } else {
        result = FileImage(file);
      }

      setState(() {
        forceLoad = true;
        widget.images = result!;
        _updateImage(widget.images /*[selectedImage]*/); // single image
      });
    }
    await Future.delayed(delay);
  }
}

This function uses the timestamp of the file to detect changes in the wallpaper. If a change is detected, it dynamically updates the wallpaper image in the user interface. In addition, it includes error handling to avoid file overuse problems and controls the delay time to avoid excessive system calls.

Goal 3: Update the color scheme accordingly.

Although the Wallpaperwatcher function is responsible for monitoring possible changes to the wallpaper, its main function is to initialize the update process. Once a change in the wallpaper is detected, this function calls _updateImage(), which in turn is responsible for updating the color scheme and loading the new wallpaper image using the buildImage() function.

Future<void> _updateImage(ImageProvider provider) async {
  final ColorScheme newColorScheme = await ColorScheme.fromImageProvider(
    provider: provider,
    brightness: isLight ? Brightness.light : Brightness.dark,
  );
  setState(() {
    buildImage(); // Update the wallpaper image
    currentColorScheme = newColorScheme; // Update the color scheme
  });
}

The buildImage() function is responsible for refreshing the background, setting a new key for Flutter to remove the current background from the cache and refresh it with the new image.

Widget buildImage() {
  ImageProvider<Object> result = forceLoad
    ? MemoryImage(imagePath.readAsBytesSync()) as ImageProvider<Object>
    : FileImage(imagePath) as ImageProvider<Object>;
  return Image(
    image: result,
    key: widget.imageKey, // Sets a new key to refresh the wallpaper
  );
}

So, goal 3 actually focuses on the buildImage() function, which uses _updateImage() and Wallpaperwatcher() as intermediaries to accomplish the color scheme and wallpaper update.

Based on the code provided by the flutter documentation, I made an adaptation of it that is able to change the color scheme dynamically (the commented code is part of the original code, I have kept it as a reference).

Real Code (Windows)
import 'dart:io';
import 'package:flutter/material.dart';

/// Flutter code sample for [ColorScheme.fromImageProvider] with content-based dynamic color.

const Widget divider = SizedBox(height: 10);
const double narrowScreenWidthThreshold = 400;

File getWallpaperFile() {
  String wallpaperPath = '';
  if (Platform.isWindows) {
    wallpaperPath =
        '${Platform.environment['APPDATA']}\\Microsoft\\Windows\\Themes\\transcodedWallpaper';
  } else if (Platform.isMacOS) {
    wallpaperPath =
        '${Platform.environment['HOME']}/Library/Application Support/Dock/desktoppicture.db';
  } else if (Platform.isLinux) {
    // Código para obtener el fondo en plataforma Linux
  } else if (Platform.isAndroid) {
    // Código para obtener el fondo en plataforma Android
  } else if (Platform.isIOS) {
    // Código para obtener el fondo en plataforma iOS
  } else if (Platform.isFuchsia) {
    // Código para obtener el fondo en plataforma Fuschia
  }

  if (wallpaperPath.isNotEmpty) {
    return File(wallpaperPath);
  } else {
    return File('');
  }
}

void main() {
  runApp(DynamicColorExample());
}

class DynamicColorExample extends StatefulWidget {
  /*
  final List<ImageProvider> images = <NetworkImage>[
    const NetworkImage(
        'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_1.png'),
    const NetworkImage(
        'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_2.png'),
    const NetworkImage(
        'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_3.png'),
    const NetworkImage(
        'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_4.png'),
    const NetworkImage(
        'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_5.png'),
    const NetworkImage(
        'https://flutter.github.io/assets-for-api-docs/assets/material/content_based_color_scheme_6.png'),
  ];
  */
  late ImageProvider images;
  late GlobalKey imageKey;

  DynamicColorExample({super.key});

  @override
  State<DynamicColorExample> createState() => _DynamicColorExampleState();
}

class _DynamicColorExampleState extends State<DynamicColorExample> {
  late ColorScheme currentColorScheme;
  String currentHyperlinkImage = '';
  late int selectedImage;
  late bool isLight;
  late bool isLoading;
  bool forceLoad = false;
  late File imagePath;
  @override
  void initState() {
    super.initState();
    widget.imageKey = GlobalKey();

    File wallpaperFile = getWallpaperFile();
    imagePath = getWallpaperFile();
    wallpaperwatcher(imagePath);
    widget.images = FileImage(wallpaperFile);

    selectedImage = 0;
    isLight = true;
    isLoading = true;

    currentColorScheme = const ColorScheme.light();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _updateImage(widget.images /*[selectedImage]*/); // single image
      isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = currentColorScheme;
    final Color selectedColor = currentColorScheme.primary;

    final ThemeData lightTheme = ThemeData(
      colorSchemeSeed: selectedColor,
      brightness: Brightness.light,
      useMaterial3: false,
    );
    final ThemeData darkTheme = ThemeData(
      colorSchemeSeed: selectedColor,
      brightness: Brightness.dark,
      useMaterial3: false,
    );

    Widget schemeLabel(String brightness, ColorScheme colorScheme) {
      return Padding(
        padding: const EdgeInsets.symmetric(vertical: 15),
        child: Text(
          brightness,
          style: TextStyle(
              fontWeight: FontWeight.bold,
              color: colorScheme.onSecondaryContainer),
        ),
      );
    }

    Widget schemeView(ThemeData theme) {
      return Padding(
        padding: const EdgeInsets.symmetric(horizontal: 15),
        child: ColorSchemeView(colorScheme: theme.colorScheme),
      );
    }

    return MaterialApp(
      theme: ThemeData(useMaterial3: true, colorScheme: colorScheme),
      debugShowCheckedModeBanner: false,
      home: Builder(
        builder: (BuildContext context) => Scaffold(
          appBar: AppBar(
            title: const Text('Content Based Dynamic Color'),
            backgroundColor: colorScheme.primary,
            foregroundColor: colorScheme.onPrimary,
            actions: <Widget>[
              const Icon(Icons.light_mode),
              Switch(
                  activeColor: colorScheme.primary,
                  activeTrackColor: colorScheme.surface,
                  inactiveTrackColor: colorScheme.onSecondary,
                  value: isLight,
                  onChanged: (bool value) {
                    setState(() {
                      isLight = value;
                      _updateImage(
                          widget.images /*[selectedImage]*/); // single image
                    });
                  })
            ],
          ),
          body: Center(
            child: isLoading
                ? const CircularProgressIndicator()
                : ColoredBox(
                    color: colorScheme.secondaryContainer,
                    child: Column(
                      children: <Widget>[
                        divider,
                        /*_imagesRow*/
                        _imageWidget(
                          context,
                          widget.images,
                          colorScheme,
                        ),
                        divider,
                        Expanded(
                          child: ColoredBox(
                            color: colorScheme.background,
                            child: LayoutBuilder(builder: (BuildContext context,
                                BoxConstraints constraints) {
                              if (constraints.maxWidth <
                                  narrowScreenWidthThreshold) {
                                return SingleChildScrollView(
                                  child: Column(
                                    children: <Widget>[
                                      divider,
                                      schemeLabel(
                                          'Light ColorScheme', colorScheme),
                                      schemeView(lightTheme),
                                      divider,
                                      divider,
                                      schemeLabel(
                                          'Dark ColorScheme', colorScheme),
                                      schemeView(darkTheme),
                                    ],
                                  ),
                                );
                              } else {
                                return SingleChildScrollView(
                                  child: Padding(
                                    padding: const EdgeInsets.only(top: 5),
                                    child: Column(
                                      children: <Widget>[
                                        Row(
                                          children: <Widget>[
                                            Expanded(
                                              child: Column(
                                                children: <Widget>[
                                                  schemeLabel(
                                                      'Light ColorScheme',
                                                      colorScheme),
                                                  schemeView(lightTheme),
                                                ],
                                              ),
                                            ),
                                            Expanded(
                                              child: Column(
                                                children: <Widget>[
                                                  schemeLabel(
                                                      'Dark ColorScheme',
                                                      colorScheme),
                                                  schemeView(darkTheme),
                                                ],
                                              ),
                                            ),
                                          ],
                                        ),
                                      ],
                                    ),
                                  ),
                                );
                              }
                            }),
                          ),
                        ),
                      ],
                    ),
                  ),
          ),
        ),
      ),
    );
  }

  Future<void> _updateImage(ImageProvider provider) async {
    final ColorScheme newColorScheme = await ColorScheme.fromImageProvider(
        provider: provider,
        brightness: isLight ? Brightness.light : Brightness.dark);
    setState(() {
      // selectedImage = widget.images.indexOf(provider);// Disabled for single image
      buildImage();
      currentColorScheme = newColorScheme;
    });
  }

  // For small screens, have two rows of image selection. For wide screens,
  // fit them onto one row.
  /*
  Widget _imagesRow(BuildContext context, List<ImageProvider> images,
      ColorScheme colorScheme) {
    final double windowHeight = MediaQuery.of(context).size.height;
    final double windowWidth = MediaQuery.of(context).size.width;
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
        if (constraints.maxWidth > 800) {
          return _adaptiveLayoutImagesRow(images, colorScheme, windowHeight);
        } else {
          return Column(children: <Widget>[
            _adaptiveLayoutImagesRow(
                images.sublist(0, 3), colorScheme, windowWidth),
            _adaptiveLayoutImagesRow(
                images.sublist(3), colorScheme, windowWidth),
          ]);
        }
      }),
    );
  }
  */

  Widget _imageWidget(
      BuildContext context, ImageProvider image, ColorScheme colorScheme) {
    final double windowWidth = MediaQuery.of(context).size.width;

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          return ConstrainedBox(
            constraints: BoxConstraints(maxWidth: windowWidth * 0.25),
            child: Card(
              color: colorScheme.primaryContainer,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(8.0),
              ),
              child: Padding(
                padding: const EdgeInsets.all(5.0),
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(8.0),
                  child: Image(
                    key: widget.imageKey,
                    image: image,
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }

  Widget buildImage() {
    ImageProvider<Object> result = forceLoad
        ? MemoryImage(imagePath.readAsBytesSync()) as ImageProvider<Object>
        : FileImage(imagePath) as ImageProvider<Object>;
    return Image(
      image: result,
      key: widget.imageKey,
    );
  }

  Future<void> wallpaperwatcher(File file) async {
    DateTime lastModified = file.statSync().modified;
    Duration delay = const Duration(milliseconds: 100);

    while (true) {
      final currentModified = file.statSync().modified;
      if (currentModified != lastModified) {
        lastModified = currentModified;
        while (true) {
          await Future.delayed(delay);
          try {
            RandomAccessFile raf = await file.open(mode: FileMode.read);
            await raf.close();
            break;
          } catch (e) {
            // print("Archivo en uso, esperando...");
          }
        }

        await (widget.imageKey.currentWidget as Image).image.evict();

        ImageProvider<Object>? result;
        if (forceLoad) {
          result = MemoryImage(imagePath.readAsBytesSync());
        } else {
          result = FileImage(file);
        }

        setState(() {
          forceLoad = true;
          widget.images = result!;
          _updateImage(widget.images /*[selectedImage]*/); // single image
        });
      }
      await Future.delayed(delay);
    }
  }

  /*
  Widget _adaptiveLayoutImagesRow(
      List<ImageProvider> images, ColorScheme colorScheme, double windowWidth) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: images
          .map(
            (ImageProvider image) => Flexible(
              flex: (images.length / 3).floor(),
              child: GestureDetector(
                onTap: () => _updateImage(image),
                child: Card(
                  color: widget.images.indexOf(image) == selectedImage
                      ? colorScheme.primaryContainer
                      : colorScheme.background,
                  child: Padding(
                    padding: const EdgeInsets.all(5.0),
                    child: ConstrainedBox(
                      constraints: BoxConstraints(maxWidth: windowWidth * .25),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(8.0),
                        child: Image(image: image),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          )
          .toList(),
    );
  }
  */
}

class ColorSchemeView extends StatelessWidget {
  const ColorSchemeView({super.key, required this.colorScheme});

  final ColorScheme colorScheme;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        ColorGroup(children: <ColorChip>[
          ColorChip(
              label: 'primary',
              color: colorScheme.primary,
              onColor: colorScheme.onPrimary),
          ColorChip(
              label: 'onPrimary',
              color: colorScheme.onPrimary,
              onColor: colorScheme.primary),
          ColorChip(
              label: 'primaryContainer',
              color: colorScheme.primaryContainer,
              onColor: colorScheme.onPrimaryContainer),
          ColorChip(
              label: 'onPrimaryContainer',
              color: colorScheme.onPrimaryContainer,
              onColor: colorScheme.primaryContainer),
        ]),
        divider,
        ColorGroup(children: <ColorChip>[
          ColorChip(
              label: 'secondary',
              color: colorScheme.secondary,
              onColor: colorScheme.onSecondary),
          ColorChip(
              label: 'onSecondary',
              color: colorScheme.onSecondary,
              onColor: colorScheme.secondary),
          ColorChip(
              label: 'secondaryContainer',
              color: colorScheme.secondaryContainer,
              onColor: colorScheme.onSecondaryContainer),
          ColorChip(
              label: 'onSecondaryContainer',
              color: colorScheme.onSecondaryContainer,
              onColor: colorScheme.secondaryContainer),
        ]),
        divider,
        ColorGroup(
          children: <ColorChip>[
            ColorChip(
                label: 'tertiary',
                color: colorScheme.tertiary,
                onColor: colorScheme.onTertiary),
            ColorChip(
                label: 'onTertiary',
                color: colorScheme.onTertiary,
                onColor: colorScheme.tertiary),
            ColorChip(
                label: 'tertiaryContainer',
                color: colorScheme.tertiaryContainer,
                onColor: colorScheme.onTertiaryContainer),
            ColorChip(
                label: 'onTertiaryContainer',
                color: colorScheme.onTertiaryContainer,
                onColor: colorScheme.tertiaryContainer),
          ],
        ),
        divider,
        ColorGroup(
          children: <ColorChip>[
            ColorChip(
                label: 'error',
                color: colorScheme.error,
                onColor: colorScheme.onError),
            ColorChip(
                label: 'onError',
                color: colorScheme.onError,
                onColor: colorScheme.error),
            ColorChip(
                label: 'errorContainer',
                color: colorScheme.errorContainer,
                onColor: colorScheme.onErrorContainer),
            ColorChip(
                label: 'onErrorContainer',
                color: colorScheme.onErrorContainer,
                onColor: colorScheme.errorContainer),
          ],
        ),
        divider,
        ColorGroup(
          children: <ColorChip>[
            ColorChip(
                label: 'background',
                color: colorScheme.background,
                onColor: colorScheme.onBackground),
            ColorChip(
                label: 'onBackground',
                color: colorScheme.onBackground,
                onColor: colorScheme.background),
          ],
        ),
        divider,
        ColorGroup(
          children: <ColorChip>[
            ColorChip(
                label: 'surface',
                color: colorScheme.surface,
                onColor: colorScheme.onSurface),
            ColorChip(
                label: 'onSurface',
                color: colorScheme.onSurface,
                onColor: colorScheme.surface),
            ColorChip(
                label: 'surfaceVariant',
                color: colorScheme.surfaceVariant,
                onColor: colorScheme.onSurfaceVariant),
            ColorChip(
                label: 'onSurfaceVariant',
                color: colorScheme.onSurfaceVariant,
                onColor: colorScheme.surfaceVariant),
          ],
        ),
        divider,
        ColorGroup(
          children: <ColorChip>[
            ColorChip(label: 'outline', color: colorScheme.outline),
            ColorChip(label: 'shadow', color: colorScheme.shadow),
            ColorChip(
                label: 'inverseSurface',
                color: colorScheme.inverseSurface,
                onColor: colorScheme.onInverseSurface),
            ColorChip(
                label: 'onInverseSurface',
                color: colorScheme.onInverseSurface,
                onColor: colorScheme.inverseSurface),
            ColorChip(
                label: 'inversePrimary',
                color: colorScheme.inversePrimary,
                onColor: colorScheme.primary),
          ],
        ),
      ],
    );
  }
}

class ColorGroup extends StatelessWidget {
  const ColorGroup({super.key, required this.children});

  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child:
          Card(clipBehavior: Clip.antiAlias, child: Column(children: children)),
    );
  }
}

class ColorChip extends StatelessWidget {
  const ColorChip({
    super.key,
    required this.color,
    required this.label,
    this.onColor,
  });

  final Color color;
  final Color? onColor;
  final String label;

  static Color contrastColor(Color color) {
    final Brightness brightness = ThemeData.estimateBrightnessForColor(color);
    return switch (brightness) {
      Brightness.dark => Colors.white,
      Brightness.light => Colors.black,
    };
  }

  @override
  Widget build(BuildContext context) {
    final Color labelColor = onColor ?? contrastColor(color);
    return ColoredBox(
      color: color,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: <Expanded>[
            Expanded(child: Text(label, style: TextStyle(color: labelColor))),
          ],
        ),
      ),
    );
  }
}
ImageProvider.mp4

Describe alternatives you've considered
I couldn't find any alternative that meets the "dynamic theme" requirement. The closest we have is dynamicColors for android12 or higher, but it's not available for other platforms :L

Additional context
The only current usage scenario is on windows for dynamicColor, it still needs to be implemented for the other platforms.

@IlluminatiWave IlluminatiWave added enhancement New feature or request triage Issues that haven't been triaged labels Apr 10, 2024
@guidezpl guidezpl added p: dynamic_color and removed triage Issues that haven't been triaged labels Jul 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request p: dynamic_color
Projects
None yet
Development

No branches or pull requests

2 participants