From dd23f7aacea4e201e8e1a65096d1a02d60ae5625 Mon Sep 17 00:00:00 2001 From: Ellet <73608287+ellet0@users.noreply.github.com> Date: Sat, 18 May 2024 13:53:01 +0300 Subject: [PATCH] Feat/support latest stable flutter (#1874) * fix: temporarily remove flutter_colorpicker from pub.dev * chore: clone flutter_colorpicker from Github, add a TODO with it * fix: update color_dialog.dart to use color picker package from the lib/src/packages/flutter_colorpicker * refactor(example): remove the old android example and recreate it to get it working with the latest stable version without any warrnings * fix: format flutter_colorpicker to fix CI failure, update the android example project to use latest version of Kotlin, fix AndroidManifest string resources * fix: update the linux example to fix CI failure * ci: update build.yml as an attemp to fix building the Linux application * ci: add a todo in build.yml, remove flutter doctor check * ci: fix a typo * ci: update the name of each step --- .github/workflows/build.yml | 19 +- example/.metadata | 25 +- example/android/app/build.gradle | 47 +- .../android/app/src/main/AndroidManifest.xml | 4 +- .../app/src/main/res/values/strings.xml | 4 - example/android/build.gradle | 60 +- example/android/gradle.properties | 5 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 20 +- example/devtools_options.yaml | 1 - example/linux/CMakeLists.txt | 6 + example/linux/my_application.cc | 20 + example/pubspec.yaml | 3 - .../flutter_colorpicker.dart | 9 + .../flutter_colorpicker/src/block_picker.dart | 211 +++ .../flutter_colorpicker/src/colorpicker.dart | 891 ++++++++++ .../flutter_colorpicker/src/colors.dart | 172 ++ .../src/material_picker.dart | 384 +++++ .../flutter_colorpicker/src/palette.dart | 1523 +++++++++++++++++ .../flutter_colorpicker/src/utils.dart | 224 +++ .../toolbar/buttons/color/color_dialog.dart | 4 +- pubspec.yaml | 3 +- 22 files changed, 3499 insertions(+), 138 deletions(-) delete mode 100644 example/android/app/src/main/res/values/strings.xml delete mode 100644 example/devtools_options.yaml create mode 100644 lib/src/packages/flutter_colorpicker/flutter_colorpicker.dart create mode 100644 lib/src/packages/flutter_colorpicker/src/block_picker.dart create mode 100644 lib/src/packages/flutter_colorpicker/src/colorpicker.dart create mode 100644 lib/src/packages/flutter_colorpicker/src/colors.dart create mode 100644 lib/src/packages/flutter_colorpicker/src/material_picker.dart create mode 100644 lib/src/packages/flutter_colorpicker/src/palette.dart create mode 100644 lib/src/packages/flutter_colorpicker/src/utils.dart diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72a76630..a0344f60 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,5 @@ +# TODO: Update the workflow to build on Android, iOS, desktop (macOS and Windows) instead of just Linux and Web + name: Build the example on: @@ -16,26 +18,29 @@ jobs: channel: 'stable' cache: true - - name: Check flutter version + - name: Check Flutter Version run: flutter --version - - name: Enable Local Dev + - name: Enable Local Development Environment (use the local packages) run: ./scripts/enable_local_dev.sh - - name: Install dependencies + - name: Install Flutter Dependencies run: flutter pub get - - name: Flutter build Web + - name: Build Flutter Web Application run: flutter build web --release --verbose --dart-define=CI=true working-directory: ./example - - name: Updates APT Linux Package Lists && Upgrade + - name: Update and Upgrade APT Packages run: sudo apt update -y && sudo apt upgrade -y - - name: Install flutter Linux prerequisites + - name: Install Flutter Linux Prerequisites run: sudo apt install -y curl git unzip xz-utils zip libglu1-mesa - - name: Flutter build Linux + - name: Install Flutter Linux Desktop Dependencies + run: sudo apt install -y clang cmake git ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev + + - name: Build Flutter Linux Desktop Application run: flutter build linux --release --verbose --dart-define=CI=true working-directory: ./example diff --git a/example/.metadata b/example/.metadata index d22992ed..e724e168 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "db7ef5bf9f59442b0e200a90587e8fa5e0c6336a" + revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - - platform: android - create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - - platform: ios - create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 - platform: linux - create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - - platform: macos - create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - - platform: web - create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - - platform: windows - create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 # User provided section diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index aa36e402..b8a6b7f8 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,62 +1,53 @@ plugins { id "com.android.application" id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" } def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') +def localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> + localPropertiesFile.withReader("UTF-8") { reader -> localProperties.load(reader) } } -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = "1" } -def flutterVersionName = localProperties.getProperty('flutter.versionName') +def flutterVersionName = localProperties.getProperty("flutter.versionName") if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = "1.0" } android { - namespace "com.example.example" - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion + namespace = "com.example.example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } defaultConfig { - applicationId "com.example.example" - minSdkVersion 23 - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + applicationId = "com.example.example" + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName } buildTypes { release { - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } } flutter { - source '../..' + source = "../.." } - -dependencies {} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index bf856053..e68e61df 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ + android:label="Flutter Quill Example"> - + \ No newline at end of file diff --git a/example/android/app/src/main/res/values/strings.xml b/example/android/app/src/main/res/values/strings.xml deleted file mode 100644 index 5c509355..00000000 --- a/example/android/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - Flutter Quill Demo - \ No newline at end of file diff --git a/example/android/build.gradle b/example/android/build.gradle index 76a4bae8..d2ffbffa 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,18 +1,3 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -buildscript { - ext.kotlin_version = '1.9.21' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.2.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -20,53 +5,12 @@ allprojects { } } -rootProject.buildDir = '../build' +rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" - - // For mode details visit https://gist.github.com/ellet0/93fefb39e48c40592bda3931e05fd35c - afterEvaluate { - // check if android block is available - - if (it.hasProperty('android')) { - - if (it.android.namespace == null) { - def manifest = new XmlSlurper().parse(file(it.android.sourceSets.main.manifest.srcFile)) - def packageName = manifest.@package.text() - println("Setting ${packageName} as android namespace in build.gradle from the AndroidManifest.xml") - android.namespace = packageName - } - - def javaVersion = JavaVersion.VERSION_17 - println("Changes will be applied for the following packages:") - android { - def androidApiVersion = 34 -// compileSdkVersion androidApiVersion - compileSdk androidApiVersion - defaultConfig { - targetSdkVersion androidApiVersion - } - compileOptions { - sourceCompatibility javaVersion - targetCompatibility javaVersion - } - tasks.withType(KotlinCompile).configureEach { - buildscript { - ext.kotlin_version = kotlin_version - } - kotlinOptions { - jvmTarget = javaVersion.toString() - } - } - String message = "For package ${android.namespace} by update compileSdkVersion, targetSdkVersion \n to $androidApiVersion and java version to ${javaVersion.toString()}" - println(message) - } - } - - } } subprojects { - project.evaluationDependsOn(':app') + project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { diff --git a/example/android/gradle.properties b/example/android/gradle.properties index b9a9a246..3b5b324f 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,6 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true -android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false -android.nonFinalResIds=false diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index aa49780c..e1ca574e 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 55c4ca8b..ee3c79c1 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -5,16 +5,22 @@ pluginManagement { def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() + }() - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + repositories { + google() + mavenCentral() + gradlePluginPortal() } } -include ":app" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + // TODO: We should update the project to not require higher version of Kotlin + id "org.jetbrains.kotlin.android" version "1.9.24" apply false +} -apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ":app" diff --git a/example/devtools_options.yaml b/example/devtools_options.yaml deleted file mode 100644 index 7e7e7f67..00000000 --- a/example/devtools_options.yaml +++ /dev/null @@ -1 +0,0 @@ -extensions: diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt index d67bd4e0..9cb0d1dd 100644 --- a/example/linux/CMakeLists.txt +++ b/example/linux/CMakeLists.txt @@ -123,6 +123,12 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) COMPONENT Runtime) endforeach(bundled_library) +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc index 0ba8f430..c0530d42 100644 --- a/example/linux/my_application.cc +++ b/example/linux/my_application.cc @@ -81,6 +81,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch return TRUE; } +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); @@ -91,6 +109,8 @@ static void my_application_dispose(GObject* object) { static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 360a03ab..85d52bf0 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -63,9 +63,6 @@ dependency_overrides: quill_pdf_converter: path: ../quill_pdf_converter - # TODO: Temporarily add this so the example can work - flutter_colorpicker: - git: https://github.com/mchome/flutter_colorpicker.git pdf_widget_wrapper: ^1.0.4 diff --git a/lib/src/packages/flutter_colorpicker/flutter_colorpicker.dart b/lib/src/packages/flutter_colorpicker/flutter_colorpicker.dart new file mode 100644 index 00000000..fee43157 --- /dev/null +++ b/lib/src/packages/flutter_colorpicker/flutter_colorpicker.dart @@ -0,0 +1,9 @@ +library flutter_colorpicker; + +// TODO: temporarily clone https://pub.dev/packages/flutter_colorpicker as it's hasn't been published on pub.dev for a while + +export 'src/block_picker.dart'; +export 'src/colorpicker.dart'; +export 'src/material_picker.dart'; +export 'src/palette.dart'; +export 'src/utils.dart'; diff --git a/lib/src/packages/flutter_colorpicker/src/block_picker.dart b/lib/src/packages/flutter_colorpicker/src/block_picker.dart new file mode 100644 index 00000000..fd6c488b --- /dev/null +++ b/lib/src/packages/flutter_colorpicker/src/block_picker.dart @@ -0,0 +1,211 @@ +// ignore_for_file: type=lint + +/// Blocky Color Picker + +library block_colorpicker; + +import 'package:flutter/material.dart'; +import 'utils.dart'; + +/// Child widget for layout builder. +typedef PickerItem = Widget Function(Color color); + +/// Customize the layout. +typedef PickerLayoutBuilder = Widget Function( + BuildContext context, List colors, PickerItem child); + +/// Customize the item shape. +typedef PickerItemBuilder = Widget Function( + Color color, bool isCurrentColor, void Function() changeColor); + +// Provide a list of colors for block color picker. +const List _defaultColors = [ + Colors.red, + Colors.pink, + Colors.purple, + Colors.deepPurple, + Colors.indigo, + Colors.blue, + Colors.lightBlue, + Colors.cyan, + Colors.teal, + Colors.green, + Colors.lightGreen, + Colors.lime, + Colors.yellow, + Colors.amber, + Colors.orange, + Colors.deepOrange, + Colors.brown, + Colors.grey, + Colors.blueGrey, + Colors.black, +]; + +// Provide a layout for [BlockPicker]. +Widget _defaultLayoutBuilder( + BuildContext context, List colors, PickerItem child) { + Orientation orientation = MediaQuery.of(context).orientation; + + return SizedBox( + width: 300, + height: orientation == Orientation.portrait ? 360 : 200, + child: GridView.count( + crossAxisCount: orientation == Orientation.portrait ? 4 : 6, + crossAxisSpacing: 5, + mainAxisSpacing: 5, + children: [for (Color color in colors) child(color)], + ), + ); +} + +// Provide a shape for [BlockPicker]. +Widget _defaultItemBuilder( + Color color, bool isCurrentColor, void Function() changeColor) { + return Container( + margin: const EdgeInsets.all(7), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.8), + offset: const Offset(1, 2), + blurRadius: 5) + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: changeColor, + borderRadius: BorderRadius.circular(50), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 210), + opacity: isCurrentColor ? 1 : 0, + child: Icon(Icons.done, + color: useWhiteForeground(color) ? Colors.white : Colors.black), + ), + ), + ), + ); +} + +// The blocky color picker you can alter the layout and shape. +class BlockPicker extends StatefulWidget { + const BlockPicker({ + Key? key, + required this.pickerColor, + required this.onColorChanged, + this.availableColors = _defaultColors, + this.useInShowDialog = true, + this.layoutBuilder = _defaultLayoutBuilder, + this.itemBuilder = _defaultItemBuilder, + }) : super(key: key); + + final Color? pickerColor; + final ValueChanged onColorChanged; + final List availableColors; + final bool useInShowDialog; + final PickerLayoutBuilder layoutBuilder; + final PickerItemBuilder itemBuilder; + + @override + State createState() => _BlockPickerState(); +} + +class _BlockPickerState extends State { + Color? _currentColor; + + @override + void initState() { + _currentColor = widget.pickerColor; + super.initState(); + } + + void changeColor(Color color) { + setState(() => _currentColor = color); + widget.onColorChanged(color); + } + + @override + Widget build(BuildContext context) { + return widget.layoutBuilder( + context, + widget.availableColors, + (Color color) => widget.itemBuilder( + color, + (_currentColor != null && + (widget.useInShowDialog ? true : widget.pickerColor != null)) + ? (_currentColor?.value == color.value) && + (widget.useInShowDialog + ? true + : widget.pickerColor?.value == color.value) + : false, + () => changeColor(color), + ), + ); + } +} + +// The blocky color picker you can alter the layout and shape with multiple choice. +class MultipleChoiceBlockPicker extends StatefulWidget { + const MultipleChoiceBlockPicker({ + Key? key, + required this.pickerColors, + required this.onColorsChanged, + this.availableColors = _defaultColors, + this.useInShowDialog = true, + this.layoutBuilder = _defaultLayoutBuilder, + this.itemBuilder = _defaultItemBuilder, + }) : super(key: key); + + final List? pickerColors; + final ValueChanged> onColorsChanged; + final List availableColors; + final bool useInShowDialog; + final PickerLayoutBuilder layoutBuilder; + final PickerItemBuilder itemBuilder; + + @override + State createState() => _MultipleChoiceBlockPickerState(); +} + +class _MultipleChoiceBlockPickerState extends State { + List? _currentColors; + + @override + void initState() { + _currentColors = widget.pickerColors; + super.initState(); + } + + void toggleColor(Color color) { + setState(() { + if (_currentColors != null) { + _currentColors!.contains(color) + ? _currentColors!.remove(color) + : _currentColors!.add(color); + } + }); + widget.onColorsChanged(_currentColors ?? []); + } + + @override + Widget build(BuildContext context) { + return widget.layoutBuilder( + context, + widget.availableColors, + (Color color) => widget.itemBuilder( + color, + (_currentColors != null && + (widget.useInShowDialog ? true : widget.pickerColors != null)) + ? _currentColors!.contains(color) && + (widget.useInShowDialog + ? true + : widget.pickerColors!.contains(color)) + : false, + () => toggleColor(color), + ), + ); + } +} diff --git a/lib/src/packages/flutter_colorpicker/src/colorpicker.dart b/lib/src/packages/flutter_colorpicker/src/colorpicker.dart new file mode 100644 index 00000000..68c8f649 --- /dev/null +++ b/lib/src/packages/flutter_colorpicker/src/colorpicker.dart @@ -0,0 +1,891 @@ +// ignore_for_file: type=lint + +/// HSV(HSB)/HSL Color Picker example +/// +/// You can create your own layout by importing `picker.dart`. + +library hsv_picker; + +import 'package:flutter/material.dart'; +import 'palette.dart'; +import 'utils.dart'; + +/// The default layout of Color Picker. +class ColorPicker extends StatefulWidget { + const ColorPicker({ + Key? key, + required this.pickerColor, + required this.onColorChanged, + this.pickerHsvColor, + this.onHsvColorChanged, + this.paletteType = PaletteType.hsvWithHue, + this.enableAlpha = true, + @Deprecated('Use empty list in [labelTypes] to disable label.') + this.showLabel = true, + this.labelTypes = const [ + ColorLabelType.rgb, + ColorLabelType.hsv, + ColorLabelType.hsl + ], + @Deprecated( + 'Use Theme.of(context).textTheme.bodyText1 & 2 to alter text style.') + this.labelTextStyle, + this.displayThumbColor = false, + this.portraitOnly = false, + this.colorPickerWidth = 300.0, + this.pickerAreaHeightPercent = 1.0, + this.pickerAreaBorderRadius = const BorderRadius.all(Radius.zero), + this.hexInputBar = false, + this.hexInputController, + this.colorHistory, + this.onHistoryChanged, + }) : super(key: key); + + final Color pickerColor; + final ValueChanged onColorChanged; + final HSVColor? pickerHsvColor; + final ValueChanged? onHsvColorChanged; + final PaletteType paletteType; + final bool enableAlpha; + final bool showLabel; + final List labelTypes; + final TextStyle? labelTextStyle; + final bool displayThumbColor; + final bool portraitOnly; + final double colorPickerWidth; + final double pickerAreaHeightPercent; + final BorderRadius pickerAreaBorderRadius; + final bool hexInputBar; + + /// Allows setting the color using text input, via [TextEditingController]. + /// + /// Listens to [String] input and trying to convert it to the valid [Color]. + /// Contains basic validator, that requires final input to be provided + /// in one of those formats: + /// + /// * RGB + /// * #RGB + /// * RRGGBB + /// * #RRGGBB + /// * AARRGGBB + /// * #AARRGGBB + /// + /// Where: A stands for Alpha, R for Red, G for Green, and B for blue color. + /// It will only accept 3/6/8 long HEXs with an optional hash (`#`) at the beginning. + /// Allowed characters are Latin A-F case insensitive and numbers 0-9. + /// It does respect the [enableAlpha] flag, so if alpha is disabled, all inputs + /// with transparency are also converted to non-transparent color values. + /// ```dart + /// MaterialButton( + /// elevation: 3.0, + /// onPressed: () { + /// // The initial value can be provided directly to the controller. + /// final textController = + /// TextEditingController(text: '#2F19DB'); + /// showDialog( + /// context: context, + /// builder: (BuildContext context) { + /// return AlertDialog( + /// scrollable: true, + /// titlePadding: const EdgeInsets.all(0.0), + /// contentPadding: const EdgeInsets.all(0.0), + /// content: Column( + /// children: [ + /// ColorPicker( + /// pickerColor: currentColor, + /// onColorChanged: changeColor, + /// colorPickerWidth: 300.0, + /// pickerAreaHeightPercent: 0.7, + /// enableAlpha: + /// true, // hexInputController will respect it too. + /// displayThumbColor: true, + /// showLabel: true, + /// paletteType: PaletteType.hsv, + /// pickerAreaBorderRadius: const BorderRadius.only( + /// topLeft: const Radius.circular(2.0), + /// topRight: const Radius.circular(2.0), + /// ), + /// hexInputController: textController, // <- here + /// portraitOnly: true, + /// ), + /// Padding( + /// padding: const EdgeInsets.all(16), + /// /* It can be any text field, for example: + /// * TextField + /// * TextFormField + /// * CupertinoTextField + /// * EditableText + /// * any text field from 3-rd party package + /// * your own text field + /// so basically anything that supports/uses + /// a TextEditingController for an editable text. + /// */ + /// child: CupertinoTextField( + /// controller: textController, + /// // Everything below is purely optional. + /// prefix: Padding( + /// padding: const EdgeInsets.only(left: 8), + /// child: const Icon(Icons.tag), + /// ), + /// suffix: IconButton( + /// icon: + /// const Icon(Icons.content_paste_rounded), + /// onPressed: () async => + /// copyToClipboard(textController.text), + /// ), + /// autofocus: true, + /// maxLength: 9, + /// inputFormatters: [ + /// // Any custom input formatter can be passed + /// // here or use any Form validator you want. + /// UpperCaseTextFormatter(), + /// FilteringTextInputFormatter.allow( + /// RegExp(kValidHexPattern)), + /// ], + /// ), + /// ) + /// ], + /// ), + /// ); + /// }, + /// ); + /// }, + /// child: const Text('Change me via text input'), + /// color: currentColor, + /// textColor: useWhiteForeground(currentColor) + /// ? const Color(0xffffffff) + /// : const Color(0xff000000), + /// ), + /// ``` + /// + /// Do not forget to `dispose()` your [TextEditingController] if you creating + /// it inside any kind of [StatefulWidget]'s [State]. + /// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet + final TextEditingController? hexInputController; + final List? colorHistory; + final ValueChanged>? onHistoryChanged; + + @override + _ColorPickerState createState() => _ColorPickerState(); +} + +class _ColorPickerState extends State { + HSVColor currentHsvColor = const HSVColor.fromAHSV(0.0, 0.0, 0.0, 0.0); + List colorHistory = []; + + @override + void initState() { + currentHsvColor = (widget.pickerHsvColor != null) + ? widget.pickerHsvColor as HSVColor + : HSVColor.fromColor(widget.pickerColor); + // If there's no initial text in `hexInputController`, + if (widget.hexInputController?.text.isEmpty == true) { + // set it to the current's color HEX value. + widget.hexInputController?.text = colorToHex( + currentHsvColor.toColor(), + enableAlpha: widget.enableAlpha, + ); + } + // Listen to the text input, If there is an `hexInputController` provided. + widget.hexInputController?.addListener(colorPickerTextInputListener); + if (widget.colorHistory != null && widget.onHistoryChanged != null) { + colorHistory = widget.colorHistory ?? []; + } + super.initState(); + } + + @override + void didUpdateWidget(ColorPicker oldWidget) { + super.didUpdateWidget(oldWidget); + currentHsvColor = (widget.pickerHsvColor != null) + ? widget.pickerHsvColor as HSVColor + : HSVColor.fromColor(widget.pickerColor); + } + + void colorPickerTextInputListener() { + // It can't be null really, since it's only listening if the controller + // is provided, but it may help to calm the Dart analyzer in the future. + if (widget.hexInputController == null) return; + // If a user is inserting/typing any text — try to get the color value from it, + // and interpret its transparency, dependent on the widget's settings. + final Color? color = colorFromHex(widget.hexInputController!.text, + enableAlpha: widget.enableAlpha); + // If it's the valid color: + if (color != null) { + // set it as the current color and + setState(() => currentHsvColor = HSVColor.fromColor(color)); + // notify with a callback. + widget.onColorChanged(color); + if (widget.onHsvColorChanged != null) + widget.onHsvColorChanged!(currentHsvColor); + } + } + + @override + void dispose() { + widget.hexInputController?.removeListener(colorPickerTextInputListener); + super.dispose(); + } + + Widget colorPickerSlider(TrackType trackType) { + return ColorPickerSlider( + trackType, + currentHsvColor, + (HSVColor color) { + // Update text in `hexInputController` if provided. + widget.hexInputController?.text = + colorToHex(color.toColor(), enableAlpha: widget.enableAlpha); + setState(() => currentHsvColor = color); + widget.onColorChanged(currentHsvColor.toColor()); + if (widget.onHsvColorChanged != null) + widget.onHsvColorChanged!(currentHsvColor); + }, + displayThumbColor: widget.displayThumbColor, + ); + } + + void onColorChanging(HSVColor color) { + // Update text in `hexInputController` if provided. + widget.hexInputController?.text = + colorToHex(color.toColor(), enableAlpha: widget.enableAlpha); + setState(() => currentHsvColor = color); + widget.onColorChanged(currentHsvColor.toColor()); + if (widget.onHsvColorChanged != null) + widget.onHsvColorChanged!(currentHsvColor); + } + + Widget colorPicker() { + return ClipRRect( + borderRadius: widget.pickerAreaBorderRadius, + child: Padding( + padding: + EdgeInsets.all(widget.paletteType == PaletteType.hueWheel ? 10 : 0), + child: ColorPickerArea( + currentHsvColor, onColorChanging, widget.paletteType), + ), + ); + } + + Widget sliderByPaletteType() { + switch (widget.paletteType) { + case PaletteType.hsv: + case PaletteType.hsvWithHue: + case PaletteType.hsl: + case PaletteType.hslWithHue: + return colorPickerSlider(TrackType.hue); + case PaletteType.hsvWithValue: + case PaletteType.hueWheel: + return colorPickerSlider(TrackType.value); + case PaletteType.hsvWithSaturation: + return colorPickerSlider(TrackType.saturation); + case PaletteType.hslWithLightness: + return colorPickerSlider(TrackType.lightness); + case PaletteType.hslWithSaturation: + return colorPickerSlider(TrackType.saturationForHSL); + case PaletteType.rgbWithBlue: + return colorPickerSlider(TrackType.blue); + case PaletteType.rgbWithGreen: + return colorPickerSlider(TrackType.green); + case PaletteType.rgbWithRed: + return colorPickerSlider(TrackType.red); + default: + return const SizedBox(); + } + } + + @override + Widget build(BuildContext context) { + if (MediaQuery.of(context).orientation == Orientation.portrait || + widget.portraitOnly) { + return Column( + children: [ + SizedBox( + width: widget.colorPickerWidth, + height: widget.colorPickerWidth * widget.pickerAreaHeightPercent, + child: colorPicker(), + ), + Padding( + padding: const EdgeInsets.fromLTRB(15.0, 5.0, 10.0, 5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () => setState(() { + if (widget.onHistoryChanged != null && + !colorHistory.contains(currentHsvColor.toColor())) { + colorHistory.add(currentHsvColor.toColor()); + widget.onHistoryChanged!(colorHistory); + } + }), + child: ColorIndicator(currentHsvColor), + ), + Expanded( + child: Column( + children: [ + SizedBox( + height: 40.0, + width: widget.colorPickerWidth - 75.0, + child: sliderByPaletteType()), + if (widget.enableAlpha) + SizedBox( + height: 40.0, + width: widget.colorPickerWidth - 75.0, + child: colorPickerSlider(TrackType.alpha), + ), + ], + ), + ), + ], + ), + ), + if (colorHistory.isNotEmpty) + SizedBox( + width: widget.colorPickerWidth, + height: 50, + child: + ListView(scrollDirection: Axis.horizontal, children: [ + for (Color color in colorHistory) + Padding( + key: Key(color.hashCode.toString()), + padding: const EdgeInsets.fromLTRB(15, 0, 0, 10), + child: Center( + child: GestureDetector( + onTap: () => onColorChanging(HSVColor.fromColor(color)), + child: ColorIndicator(HSVColor.fromColor(color), + width: 30, height: 30), + ), + ), + ), + const SizedBox(width: 15), + ]), + ), + if (widget.showLabel && widget.labelTypes.isNotEmpty) + FittedBox( + child: ColorPickerLabel( + currentHsvColor, + enableAlpha: widget.enableAlpha, + textStyle: widget.labelTextStyle, + colorLabelTypes: widget.labelTypes, + ), + ), + if (widget.hexInputBar) + ColorPickerInput( + currentHsvColor.toColor(), + (Color color) { + setState(() => currentHsvColor = HSVColor.fromColor(color)); + widget.onColorChanged(currentHsvColor.toColor()); + if (widget.onHsvColorChanged != null) + widget.onHsvColorChanged!(currentHsvColor); + }, + enableAlpha: widget.enableAlpha, + embeddedText: false, + ), + const SizedBox(height: 20.0), + ], + ); + } else { + return Row( + children: [ + SizedBox( + width: widget.colorPickerWidth, + height: widget.colorPickerWidth * widget.pickerAreaHeightPercent, + child: colorPicker()), + Column( + children: [ + Row( + children: [ + const SizedBox(width: 20.0), + GestureDetector( + onTap: () => setState(() { + if (widget.onHistoryChanged != null && + !colorHistory.contains(currentHsvColor.toColor())) { + colorHistory.add(currentHsvColor.toColor()); + widget.onHistoryChanged!(colorHistory); + } + }), + child: ColorIndicator(currentHsvColor), + ), + Column( + children: [ + SizedBox( + height: 40.0, + width: 260.0, + child: sliderByPaletteType()), + if (widget.enableAlpha) + SizedBox( + height: 40.0, + width: 260.0, + child: colorPickerSlider(TrackType.alpha)), + ], + ), + const SizedBox(width: 10.0), + ], + ), + if (colorHistory.isNotEmpty) + SizedBox( + width: widget.colorPickerWidth, + height: 50, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (Color color in colorHistory) + Padding( + key: Key(color.hashCode.toString()), + padding: const EdgeInsets.fromLTRB(15, 18, 0, 0), + child: Center( + child: GestureDetector( + onTap: () => + onColorChanging(HSVColor.fromColor(color)), + onLongPress: () { + if (colorHistory.remove(color)) { + widget.onHistoryChanged!(colorHistory); + setState(() {}); + } + }, + child: ColorIndicator(HSVColor.fromColor(color), + width: 30, height: 30), + ), + ), + ), + const SizedBox(width: 15), + ]), + ), + const SizedBox(height: 20.0), + if (widget.showLabel && widget.labelTypes.isNotEmpty) + FittedBox( + child: ColorPickerLabel( + currentHsvColor, + enableAlpha: widget.enableAlpha, + textStyle: widget.labelTextStyle, + colorLabelTypes: widget.labelTypes, + ), + ), + if (widget.hexInputBar) + ColorPickerInput( + currentHsvColor.toColor(), + (Color color) { + setState(() => currentHsvColor = HSVColor.fromColor(color)); + widget.onColorChanged(currentHsvColor.toColor()); + if (widget.onHsvColorChanged != null) + widget.onHsvColorChanged!(currentHsvColor); + }, + enableAlpha: widget.enableAlpha, + embeddedText: false, + ), + const SizedBox(height: 5), + ], + ), + ], + ); + } + } +} + +/// The Color Picker with sliders only. Support HSV, HSL and RGB color model. +class SlidePicker extends StatefulWidget { + const SlidePicker({ + Key? key, + required this.pickerColor, + required this.onColorChanged, + this.colorModel = ColorModel.rgb, + this.enableAlpha = true, + this.sliderSize = const Size(260, 40), + this.showSliderText = true, + @Deprecated( + 'Use Theme.of(context).textTheme.bodyText1 & 2 to alter text style.') + this.sliderTextStyle, + this.showParams = true, + @Deprecated('Use empty list in [labelTypes] to disable label.') + this.showLabel = true, + this.labelTypes = const [], + @Deprecated( + 'Use Theme.of(context).textTheme.bodyText1 & 2 to alter text style.') + this.labelTextStyle, + this.showIndicator = true, + this.indicatorSize = const Size(280, 50), + this.indicatorAlignmentBegin = const Alignment(-1.0, -3.0), + this.indicatorAlignmentEnd = const Alignment(1.0, 3.0), + this.displayThumbColor = true, + this.indicatorBorderRadius = const BorderRadius.all(Radius.zero), + }) : super(key: key); + + final Color pickerColor; + final ValueChanged onColorChanged; + final ColorModel colorModel; + final bool enableAlpha; + final Size sliderSize; + final bool showSliderText; + final TextStyle? sliderTextStyle; + final bool showLabel; + final bool showParams; + final List labelTypes; + final TextStyle? labelTextStyle; + final bool showIndicator; + final Size indicatorSize; + final AlignmentGeometry indicatorAlignmentBegin; + final AlignmentGeometry indicatorAlignmentEnd; + final bool displayThumbColor; + final BorderRadius indicatorBorderRadius; + + @override + State createState() => _SlidePickerState(); +} + +class _SlidePickerState extends State { + HSVColor currentHsvColor = const HSVColor.fromAHSV(0.0, 0.0, 0.0, 0.0); + + @override + void initState() { + super.initState(); + currentHsvColor = HSVColor.fromColor(widget.pickerColor); + } + + @override + void didUpdateWidget(SlidePicker oldWidget) { + super.didUpdateWidget(oldWidget); + currentHsvColor = HSVColor.fromColor(widget.pickerColor); + } + + Widget colorPickerSlider(TrackType trackType) { + return ColorPickerSlider( + trackType, + currentHsvColor, + (HSVColor color) { + setState(() => currentHsvColor = color); + widget.onColorChanged(currentHsvColor.toColor()); + }, + displayThumbColor: widget.displayThumbColor, + fullThumbColor: true, + ); + } + + Widget indicator() { + return ClipRRect( + borderRadius: widget.indicatorBorderRadius, + clipBehavior: Clip.antiAliasWithSaveLayer, + child: GestureDetector( + onTap: () { + setState( + () => currentHsvColor = HSVColor.fromColor(widget.pickerColor)); + widget.onColorChanged(currentHsvColor.toColor()); + }, + child: Container( + width: widget.indicatorSize.width, + height: widget.indicatorSize.height, + margin: const EdgeInsets.only(bottom: 15.0), + foregroundDecoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + widget.pickerColor, + widget.pickerColor, + currentHsvColor.toColor(), + currentHsvColor.toColor(), + ], + begin: widget.indicatorAlignmentBegin, + end: widget.indicatorAlignmentEnd, + stops: const [0.0, 0.5, 0.5, 1.0], + ), + ), + child: const CustomPaint(painter: CheckerPainter()), + ), + ), + ); + } + + String getColorParams(int pos) { + assert(pos >= 0 && pos < 4); + if (widget.colorModel == ColorModel.rgb) { + final Color color = currentHsvColor.toColor(); + return [ + color.red.toString(), + color.green.toString(), + color.blue.toString(), + '${(color.opacity * 100).round()}', + ][pos]; + } else if (widget.colorModel == ColorModel.hsv) { + return [ + currentHsvColor.hue.round().toString(), + (currentHsvColor.saturation * 100).round().toString(), + (currentHsvColor.value * 100).round().toString(), + (currentHsvColor.alpha * 100).round().toString(), + ][pos]; + } else if (widget.colorModel == ColorModel.hsl) { + HSLColor hslColor = hsvToHsl(currentHsvColor); + return [ + hslColor.hue.round().toString(), + (hslColor.saturation * 100).round().toString(), + (hslColor.lightness * 100).round().toString(), + (currentHsvColor.alpha * 100).round().toString(), + ][pos]; + } else { + return '??'; + } + } + + @override + Widget build(BuildContext context) { + double fontSize = 14; + if (widget.labelTextStyle != null && + widget.labelTextStyle?.fontSize != null) { + fontSize = widget.labelTextStyle?.fontSize ?? 14; + } + final List trackTypes = [ + if (widget.colorModel == ColorModel.hsv) ...[ + TrackType.hue, + TrackType.saturation, + TrackType.value + ], + if (widget.colorModel == ColorModel.hsl) ...[ + TrackType.hue, + TrackType.saturationForHSL, + TrackType.lightness + ], + if (widget.colorModel == ColorModel.rgb) ...[ + TrackType.red, + TrackType.green, + TrackType.blue + ], + if (widget.enableAlpha) ...[TrackType.alpha], + ]; + List sliders = [ + for (TrackType trackType in trackTypes) + SizedBox( + width: widget.sliderSize.width, + height: widget.sliderSize.height, + child: Row( + children: [ + if (widget.showSliderText) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Text( + trackType.toString().split('.').last[0].toUpperCase(), + style: widget.sliderTextStyle ?? + Theme.of(context).textTheme.bodyLarge, + ), + ), + Expanded(child: colorPickerSlider(trackType)), + if (widget.showParams) + ConstrainedBox( + constraints: BoxConstraints(minWidth: fontSize * 2 + 5), + child: Text( + getColorParams(trackTypes.indexOf(trackType)), + style: widget.sliderTextStyle ?? + Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.right, + ), + ), + ], + ), + ), + ]; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (widget.showIndicator) indicator(), + if (!widget.showIndicator) const SizedBox(height: 20), + ...sliders, + const SizedBox(height: 20.0), + if (widget.showLabel && widget.labelTypes.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 20.0), + child: ColorPickerLabel( + currentHsvColor, + enableAlpha: widget.enableAlpha, + textStyle: widget.labelTextStyle, + colorLabelTypes: widget.labelTypes, + ), + ), + ], + ); + } +} + +/// The Color Picker with HUE Ring & HSV model. +class HueRingPicker extends StatefulWidget { + const HueRingPicker({ + Key? key, + required this.pickerColor, + required this.onColorChanged, + this.portraitOnly = false, + this.colorPickerHeight = 250.0, + this.hueRingStrokeWidth = 20.0, + this.enableAlpha = false, + this.displayThumbColor = true, + this.pickerAreaBorderRadius = const BorderRadius.all(Radius.zero), + }) : super(key: key); + + final Color pickerColor; + final ValueChanged onColorChanged; + final bool portraitOnly; + final double colorPickerHeight; + final double hueRingStrokeWidth; + final bool enableAlpha; + final bool displayThumbColor; + final BorderRadius pickerAreaBorderRadius; + + @override + _HueRingPickerState createState() => _HueRingPickerState(); +} + +class _HueRingPickerState extends State { + HSVColor currentHsvColor = const HSVColor.fromAHSV(0.0, 0.0, 0.0, 0.0); + + @override + void initState() { + currentHsvColor = HSVColor.fromColor(widget.pickerColor); + super.initState(); + } + + @override + void didUpdateWidget(HueRingPicker oldWidget) { + super.didUpdateWidget(oldWidget); + currentHsvColor = HSVColor.fromColor(widget.pickerColor); + } + + void onColorChanging(HSVColor color) { + setState(() => currentHsvColor = color); + widget.onColorChanged(currentHsvColor.toColor()); + } + + @override + Widget build(BuildContext context) { + if (MediaQuery.of(context).orientation == Orientation.portrait || + widget.portraitOnly) { + return Column( + children: [ + ClipRRect( + borderRadius: widget.pickerAreaBorderRadius, + child: Padding( + padding: const EdgeInsets.all(15), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + SizedBox( + width: widget.colorPickerHeight, + height: widget.colorPickerHeight, + child: ColorPickerHueRing( + currentHsvColor, + onColorChanging, + displayThumbColor: widget.displayThumbColor, + strokeWidth: widget.hueRingStrokeWidth, + ), + ), + SizedBox( + width: widget.colorPickerHeight / 1.6, + height: widget.colorPickerHeight / 1.6, + child: ColorPickerArea( + currentHsvColor, onColorChanging, PaletteType.hsv), + ) + ]), + ), + ), + if (widget.enableAlpha) + SizedBox( + height: 40.0, + width: widget.colorPickerHeight, + child: ColorPickerSlider( + TrackType.alpha, + currentHsvColor, + onColorChanging, + displayThumbColor: widget.displayThumbColor, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(15.0, 5.0, 10.0, 5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 10), + ColorIndicator(currentHsvColor), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(0, 5, 0, 20), + child: ColorPickerInput( + currentHsvColor.toColor(), + (Color color) { + setState( + () => currentHsvColor = HSVColor.fromColor(color)); + widget.onColorChanged(currentHsvColor.toColor()); + }, + enableAlpha: widget.enableAlpha, + embeddedText: true, + ), + ), + ), + ], + ), + ), + ], + ); + } else { + return Row( + children: [ + Expanded( + child: SizedBox( + width: 300.0, + height: widget.colorPickerHeight, + child: ClipRRect( + borderRadius: widget.pickerAreaBorderRadius, + child: ColorPickerArea( + currentHsvColor, onColorChanging, PaletteType.hsv), + ), + ), + ), + ClipRRect( + borderRadius: widget.pickerAreaBorderRadius, + child: Padding( + padding: const EdgeInsets.all(15), + child: Stack( + alignment: AlignmentDirectional.topCenter, + children: [ + SizedBox( + width: widget.colorPickerHeight - + widget.hueRingStrokeWidth * 2, + height: widget.colorPickerHeight - + widget.hueRingStrokeWidth * 2, + child: ColorPickerHueRing( + currentHsvColor, onColorChanging, + strokeWidth: widget.hueRingStrokeWidth), + ), + Column( + children: [ + SizedBox(height: widget.colorPickerHeight / 8.5), + ColorIndicator(currentHsvColor), + const SizedBox(height: 10), + ColorPickerInput( + currentHsvColor.toColor(), + (Color color) { + setState(() => + currentHsvColor = HSVColor.fromColor(color)); + widget.onColorChanged(currentHsvColor.toColor()); + }, + enableAlpha: widget.enableAlpha, + embeddedText: true, + disable: true, + ), + if (widget.enableAlpha) const SizedBox(height: 5), + if (widget.enableAlpha) + SizedBox( + height: 40.0, + width: (widget.colorPickerHeight - + widget.hueRingStrokeWidth * 2) / + 2, + child: ColorPickerSlider( + TrackType.alpha, + currentHsvColor, + onColorChanging, + displayThumbColor: true, + ), + ), + ], + ), + ]), + ), + ), + ], + ); + } + } +} diff --git a/lib/src/packages/flutter_colorpicker/src/colors.dart b/lib/src/packages/flutter_colorpicker/src/colors.dart new file mode 100644 index 00000000..041a1f20 --- /dev/null +++ b/lib/src/packages/flutter_colorpicker/src/colors.dart @@ -0,0 +1,172 @@ +// ignore_for_file: type=lint + +import 'dart:ui'; + +/// X11 Colors +/// +/// https://en.wikipedia.org/wiki/X11_color_names + +const Map x11Colors = { + 'aliceblue': Color(0xfff0f8ff), + 'antiquewhite': Color(0xfffaebd7), + 'aqua': Color(0xff00ffff), + 'aquamarine': Color(0xff7fffd4), + 'azure': Color(0xfff0ffff), + 'beige': Color(0xfff5f5dc), + 'bisque': Color(0xffffe4c4), + 'black': Color(0xff000000), + 'blanchedalmond': Color(0xffffebcd), + 'blue': Color(0xff0000ff), + 'blueviolet': Color(0xff8a2be2), + 'brown': Color(0xffa52a2a), + 'burlywood': Color(0xffdeb887), + 'cadetblue': Color(0xff5f9ea0), + 'chartreuse': Color(0xff7fff00), + 'chocolate': Color(0xffd2691e), + 'coral': Color(0xffff7f50), + 'cornflower': Color(0xff6495ed), + 'cornflowerblue': Color(0xff6495ed), + 'cornsilk': Color(0xfffff8dc), + 'crimson': Color(0xffdc143c), + 'cyan': Color(0xff00ffff), + 'darkblue': Color(0xff00008b), + 'darkcyan': Color(0xff008b8b), + 'darkgoldenrod': Color(0xffb8860b), + 'darkgray': Color(0xffa9a9a9), + 'darkgreen': Color(0xff006400), + 'darkgrey': Color(0xffa9a9a9), + 'darkkhaki': Color(0xffbdb76b), + 'darkmagenta': Color(0xff8b008b), + 'darkolivegreen': Color(0xff556b2f), + 'darkorange': Color(0xffff8c00), + 'darkorchid': Color(0xff9932cc), + 'darkred': Color(0xff8b0000), + 'darksalmon': Color(0xffe9967a), + 'darkseagreen': Color(0xff8fbc8f), + 'darkslateblue': Color(0xff483d8b), + 'darkslategray': Color(0xff2f4f4f), + 'darkslategrey': Color(0xff2f4f4f), + 'darkturquoise': Color(0xff00ced1), + 'darkviolet': Color(0xff9400d3), + 'deeppink': Color(0xffff1493), + 'deepskyblue': Color(0xff00bfff), + 'dimgray': Color(0xff696969), + 'dimgrey': Color(0xff696969), + 'dodgerblue': Color(0xff1e90ff), + 'firebrick': Color(0xffb22222), + 'floralwhite': Color(0xfffffaf0), + 'forestgreen': Color(0xff228b22), + 'fuchsia': Color(0xffff00ff), + 'gainsboro': Color(0xffdcdcdc), + 'ghostwhite': Color(0xfff8f8ff), + 'gold': Color(0xffffd700), + 'goldenrod': Color(0xffdaa520), + 'gray': Color(0xff808080), + 'green': Color(0xff008000), + 'greenyellow': Color(0xffadff2f), + 'grey': Color(0xff808080), + 'honeydew': Color(0xfff0fff0), + 'hotpink': Color(0xffff69b4), + 'indianred': Color(0xffcd5c5c), + 'indigo': Color(0xff4b0082), + 'ivory': Color(0xfffffff0), + 'khaki': Color(0xfff0e68c), + 'laserlemon': Color(0xffffff54), + 'lavender': Color(0xffe6e6fa), + 'lavenderblush': Color(0xfffff0f5), + 'lawngreen': Color(0xff7cfc00), + 'lemonchiffon': Color(0xfffffacd), + 'lightblue': Color(0xffadd8e6), + 'lightcoral': Color(0xfff08080), + 'lightcyan': Color(0xffe0ffff), + 'lightgoldenrod': Color(0xfffafad2), + 'lightgoldenrodyellow': Color(0xfffafad2), + 'lightgray': Color(0xffd3d3d3), + 'lightgreen': Color(0xff90ee90), + 'lightgrey': Color(0xffd3d3d3), + 'lightpink': Color(0xffffb6c1), + 'lightsalmon': Color(0xffffa07a), + 'lightseagreen': Color(0xff20b2aa), + 'lightskyblue': Color(0xff87cefa), + 'lightslategray': Color(0xff778899), + 'lightslategrey': Color(0xff778899), + 'lightsteelblue': Color(0xffb0c4de), + 'lightyellow': Color(0xffffffe0), + 'lime': Color(0xff00ff00), + 'limegreen': Color(0xff32cd32), + 'linen': Color(0xfffaf0e6), + 'magenta': Color(0xffff00ff), + 'maroon': Color(0xff800000), + 'maroon2': Color(0xff7f0000), + 'maroon3': Color(0xffb03060), + 'mediumaquamarine': Color(0xff66cdaa), + 'mediumblue': Color(0xff0000cd), + 'mediumorchid': Color(0xffba55d3), + 'mediumpurple': Color(0xff9370db), + 'mediumseagreen': Color(0xff3cb371), + 'mediumslateblue': Color(0xff7b68ee), + 'mediumspringgreen': Color(0xff00fa9a), + 'mediumturquoise': Color(0xff48d1cc), + 'mediumvioletred': Color(0xffc71585), + 'midnightblue': Color(0xff191970), + 'mintcream': Color(0xfff5fffa), + 'mistyrose': Color(0xffffe4e1), + 'moccasin': Color(0xffffe4b5), + 'navajowhite': Color(0xffffdead), + 'navy': Color(0xff000080), + 'oldlace': Color(0xfffdf5e6), + 'olive': Color(0xff808000), + 'olivedrab': Color(0xff6b8e23), + 'orange': Color(0xffffa500), + 'orangered': Color(0xffff4500), + 'orchid': Color(0xffda70d6), + 'palegoldenrod': Color(0xffeee8aa), + 'palegreen': Color(0xff98fb98), + 'paleturquoise': Color(0xffafeeee), + 'palevioletred': Color(0xffdb7093), + 'papayawhip': Color(0xffffefd5), + 'peachpuff': Color(0xffffdab9), + 'peru': Color(0xffcd853f), + 'pink': Color(0xffffc0cb), + 'plum': Color(0xffdda0dd), + 'powderblue': Color(0xffb0e0e6), + 'purple': Color(0xff800080), + 'purple2': Color(0xff7f007f), + 'purple3': Color(0xffa020f0), + 'rebeccapurple': Color(0xff663399), + 'red': Color(0xffff0000), + 'rosybrown': Color(0xffbc8f8f), + 'royalblue': Color(0xff4169e1), + 'saddlebrown': Color(0xff8b4513), + 'salmon': Color(0xfffa8072), + 'sandybrown': Color(0xfff4a460), + 'seagreen': Color(0xff2e8b57), + 'seashell': Color(0xfffff5ee), + 'sienna': Color(0xffa0522d), + 'silver': Color(0xffc0c0c0), + 'skyblue': Color(0xff87ceeb), + 'slateblue': Color(0xff6a5acd), + 'slategray': Color(0xff708090), + 'slategrey': Color(0xff708090), + 'snow': Color(0xfffffafa), + 'springgreen': Color(0xff00ff7f), + 'steelblue': Color(0xff4682b4), + 'tan': Color(0xffd2b48c), + 'teal': Color(0xff008080), + 'thistle': Color(0xffd8bfd8), + 'tomato': Color(0xffff6347), + 'turquoise': Color(0xff40e0d0), + 'violet': Color(0xffee82ee), + 'wheat': Color(0xfff5deb3), + 'white': Color(0xffffffff), + 'whitesmoke': Color(0xfff5f5f5), + 'yellow': Color(0xffffff00), + 'yellowgreen': Color(0xff9acd32), +}; + +Color? colorFromName(String val) => + x11Colors[val.trim().replaceAll(' ', '').toLowerCase()]; + +extension ColorExtension on String { + Color? toColor() => colorFromName(this); +} diff --git a/lib/src/packages/flutter_colorpicker/src/material_picker.dart b/lib/src/packages/flutter_colorpicker/src/material_picker.dart new file mode 100644 index 00000000..b9d5d8c2 --- /dev/null +++ b/lib/src/packages/flutter_colorpicker/src/material_picker.dart @@ -0,0 +1,384 @@ +// ignore_for_file: type=lint + +/// Material Color Picker + +library material_colorpicker; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'utils.dart'; + +// The Color Picker which contains Material Design Color Palette. +class MaterialPicker extends StatefulWidget { + const MaterialPicker({ + Key? key, + required this.pickerColor, + required this.onColorChanged, + this.onPrimaryChanged, + this.enableLabel = false, + this.portraitOnly = false, + }) : super(key: key); + + final Color pickerColor; + final ValueChanged onColorChanged; + final ValueChanged? onPrimaryChanged; + final bool enableLabel; + final bool portraitOnly; + + @override + State createState() => _MaterialPickerState(); +} + +class _MaterialPickerState extends State { + final List> _colorTypes = [ + [Colors.red, Colors.redAccent], + [Colors.pink, Colors.pinkAccent], + [Colors.purple, Colors.purpleAccent], + [Colors.deepPurple, Colors.deepPurpleAccent], + [Colors.indigo, Colors.indigoAccent], + [Colors.blue, Colors.blueAccent], + [Colors.lightBlue, Colors.lightBlueAccent], + [Colors.cyan, Colors.cyanAccent], + [Colors.teal, Colors.tealAccent], + [Colors.green, Colors.greenAccent], + [Colors.lightGreen, Colors.lightGreenAccent], + [Colors.lime, Colors.limeAccent], + [Colors.yellow, Colors.yellowAccent], + [Colors.amber, Colors.amberAccent], + [Colors.orange, Colors.orangeAccent], + [Colors.deepOrange, Colors.deepOrangeAccent], + [Colors.brown], + [Colors.grey], + [Colors.blueGrey], + [Colors.black], + ]; + + List _currentColorType = [Colors.red, Colors.redAccent]; + Color _currentShading = Colors.transparent; + + List> _shadingTypes(List colors) { + List> result = []; + + for (Color colorType in colors) { + if (colorType == Colors.grey) { + result.addAll([ + 50, + 100, + 200, + 300, + 350, + 400, + 500, + 600, + 700, + 800, + 850, + 900 + ].map((int shade) => {Colors.grey[shade]!: shade.toString()}).toList()); + } else if (colorType == Colors.black || colorType == Colors.white) { + result.addAll([ + {Colors.black: ''}, + {Colors.white: ''} + ]); + } else if (colorType is MaterialAccentColor) { + result.addAll([100, 200, 400, 700] + .map((int shade) => {colorType[shade]!: 'A$shade'}) + .toList()); + } else if (colorType is MaterialColor) { + result.addAll([50, 100, 200, 300, 400, 500, 600, 700, 800, 900] + .map((int shade) => {colorType[shade]!: shade.toString()}) + .toList()); + } else { + result.add({const Color(0x00000000): ''}); + } + } + + return result; + } + + @override + void initState() { + for (List _colors in _colorTypes) { + _shadingTypes(_colors).forEach((Map color) { + if (widget.pickerColor.value == color.keys.first.value) { + return setState(() { + _currentColorType = _colors; + _currentShading = color.keys.first; + }); + } + }); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + bool _isPortrait = + MediaQuery.of(context).orientation == Orientation.portrait || + widget.portraitOnly; + + Widget _colorList() { + return Container( + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: Container( + margin: _isPortrait + ? const EdgeInsets.only(right: 10) + : const EdgeInsets.only(bottom: 10), + width: _isPortrait ? 60 : null, + height: _isPortrait ? null : 60, + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + boxShadow: [ + BoxShadow( + color: (Theme.of(context).brightness == Brightness.light) + ? (Theme.of(context).brightness == Brightness.light) + ? Colors.grey[300]! + : Colors.black38 + : Colors.black38, + blurRadius: 10) + ], + border: _isPortrait + ? Border( + right: BorderSide( + color: + (Theme.of(context).brightness == Brightness.light) + ? Colors.grey[300]! + : Colors.black38, + width: 1)) + : Border( + top: BorderSide( + color: + (Theme.of(context).brightness == Brightness.light) + ? Colors.grey[300]! + : Colors.black38, + width: 1)), + ), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(dragDevices: PointerDeviceKind.values.toSet()), + child: ListView( + scrollDirection: _isPortrait ? Axis.vertical : Axis.horizontal, + children: [ + _isPortrait + ? const Padding(padding: EdgeInsets.only(top: 7)) + : const Padding(padding: EdgeInsets.only(left: 7)), + ..._colorTypes.map((List _colors) { + Color _colorType = _colors[0]; + return GestureDetector( + onTap: () { + if (widget.onPrimaryChanged != null) + widget.onPrimaryChanged!(_colorType); + setState(() => _currentColorType = _colors); + }, + child: Container( + color: const Color(0x00000000), + padding: _isPortrait + ? const EdgeInsets.fromLTRB(0, 7, 0, 7) + : const EdgeInsets.fromLTRB(7, 0, 7, 0), + child: Align( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: 25, + height: 25, + decoration: BoxDecoration( + color: _colorType, + shape: BoxShape.circle, + boxShadow: _currentColorType == _colors + ? [ + _colorType == Theme.of(context).cardColor + ? BoxShadow( + color: + (Theme.of(context).brightness == + Brightness.light) + ? Colors.grey[300]! + : Colors.black38, + blurRadius: 10, + ) + : BoxShadow( + color: _colorType, + blurRadius: 10, + ), + ] + : null, + border: _colorType == Theme.of(context).cardColor + ? Border.all( + color: (Theme.of(context).brightness == + Brightness.light) + ? Colors.grey[300]! + : Colors.black38, + width: 1) + : null, + ), + ), + ), + ), + ); + }), + _isPortrait + ? const Padding(padding: EdgeInsets.only(top: 5)) + : const Padding(padding: EdgeInsets.only(left: 5)), + ], + ), + ), + ), + ); + } + + Widget _shadingList() { + return ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(dragDevices: PointerDeviceKind.values.toSet()), + child: ListView( + scrollDirection: _isPortrait ? Axis.vertical : Axis.horizontal, + children: [ + _isPortrait + ? const Padding(padding: EdgeInsets.only(top: 15)) + : const Padding(padding: EdgeInsets.only(left: 15)), + ..._shadingTypes(_currentColorType).map((Map color) { + final Color _color = color.keys.first; + return GestureDetector( + onTap: () { + setState(() => _currentShading = _color); + widget.onColorChanged(_color); + }, + child: Container( + color: const Color(0x00000000), + margin: _isPortrait + ? const EdgeInsets.only(right: 10) + : const EdgeInsets.only(bottom: 10), + padding: _isPortrait + ? const EdgeInsets.fromLTRB(0, 7, 0, 7) + : const EdgeInsets.fromLTRB(7, 0, 7, 0), + child: Align( + child: AnimatedContainer( + curve: Curves.fastOutSlowIn, + duration: const Duration(milliseconds: 500), + width: _isPortrait + ? (_currentShading == _color ? 250 : 230) + : (_currentShading == _color ? 50 : 30), + height: _isPortrait ? 50 : 220, + decoration: BoxDecoration( + color: _color, + boxShadow: _currentShading == _color + ? [ + (_color == Colors.white) || + (_color == Colors.black) + ? BoxShadow( + color: (Theme.of(context).brightness == + Brightness.light) + ? Colors.grey[300]! + : Colors.black38, + blurRadius: 10, + ) + : BoxShadow( + color: _color, + blurRadius: 10, + ), + ] + : null, + border: + (_color == Colors.white) || (_color == Colors.black) + ? Border.all( + color: (Theme.of(context).brightness == + Brightness.light) + ? Colors.grey[300]! + : Colors.black38, + width: 1) + : null, + ), + child: widget.enableLabel + ? _isPortrait + ? Row( + children: [ + Text( + ' ${color.values.first}', + style: TextStyle( + color: useWhiteForeground(_color) + ? Colors.white + : Colors.black), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Text( + '#${(_color.toString().replaceFirst('Color(0xff', '').replaceFirst(')', '')).toUpperCase()} ', + style: TextStyle( + color: useWhiteForeground(_color) + ? Colors.white + : Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ) + : AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: _currentShading == _color ? 1 : 0, + child: Container( + padding: const EdgeInsets.only(top: 16), + alignment: Alignment.topCenter, + child: Text( + color.values.first, + style: TextStyle( + color: useWhiteForeground(_color) + ? Colors.white + : Colors.black, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + softWrap: false, + ), + ), + ) + : const SizedBox(), + ), + ), + ), + ); + }), + _isPortrait + ? const Padding(padding: EdgeInsets.only(top: 15)) + : const Padding(padding: EdgeInsets.only(left: 15)), + ], + ), + ); + } + + if (_isPortrait) { + return SizedBox( + width: 350, + height: 500, + child: Row( + children: [ + _colorList(), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: _shadingList(), + ), + ), + ], + ), + ); + } else { + return SizedBox( + width: 500, + height: 300, + child: Column( + children: [ + _colorList(), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: _shadingList(), + ), + ), + ], + ), + ); + } + } +} diff --git a/lib/src/packages/flutter_colorpicker/src/palette.dart b/lib/src/packages/flutter_colorpicker/src/palette.dart new file mode 100644 index 00000000..5dc7c4ec --- /dev/null +++ b/lib/src/packages/flutter_colorpicker/src/palette.dart @@ -0,0 +1,1523 @@ +// ignore_for_file: type=lint + +/// The components of HSV Color Picker +/// +/// Try to create a Color Picker with other layout on your own :) + +import 'dart:math'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'utils.dart'; + +/// Palette types for color picker area widget. +enum PaletteType { + hsv, + hsvWithHue, + hsvWithValue, + hsvWithSaturation, + hsl, + hslWithHue, + hslWithLightness, + hslWithSaturation, + rgbWithBlue, + rgbWithGreen, + rgbWithRed, + hueWheel, +} + +/// Track types for slider picker. +enum TrackType { + hue, + saturation, + saturationForHSL, + value, + lightness, + red, + green, + blue, + alpha, +} + +/// Color information label type. +enum ColorLabelType { hex, rgb, hsv, hsl } + +/// Types for slider picker widget. +enum ColorModel { rgb, hsv, hsl } +// enum ColorSpace { rgb, hsv, hsl, hsp, okhsv, okhsl, xyz, yuv, lab, lch, cmyk } + +/// Painter for SV mixture. +class HSVWithHueColorPainter extends CustomPainter { + const HSVWithHueColorPainter(this.hsvColor, {this.pointerColor}); + + final HSVColor hsvColor; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + const Gradient gradientV = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.black], + ); + final Gradient gradientH = LinearGradient( + colors: [ + Colors.white, + HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(), + ], + ); + canvas.drawRect(rect, Paint()..shader = gradientV.createShader(rect)); + canvas.drawRect( + rect, + Paint() + ..blendMode = BlendMode.multiply + ..shader = gradientH.createShader(rect), + ); + + canvas.drawCircle( + Offset( + size.width * hsvColor.saturation, size.height * (1 - hsvColor.value)), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(hsvColor.toColor()) + ? Colors.white + : Colors.black) + ..strokeWidth = 1.5 + ..blendMode = BlendMode.luminosity + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for HV mixture. +class HSVWithSaturationColorPainter extends CustomPainter { + const HSVWithSaturationColorPainter(this.hsvColor, {this.pointerColor}); + + final HSVColor hsvColor; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + const Gradient gradientV = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black], + ); + final List colors = [ + HSVColor.fromAHSV(1.0, 0.0, hsvColor.saturation, 1.0).toColor(), + HSVColor.fromAHSV(1.0, 60.0, hsvColor.saturation, 1.0).toColor(), + HSVColor.fromAHSV(1.0, 120.0, hsvColor.saturation, 1.0).toColor(), + HSVColor.fromAHSV(1.0, 180.0, hsvColor.saturation, 1.0).toColor(), + HSVColor.fromAHSV(1.0, 240.0, hsvColor.saturation, 1.0).toColor(), + HSVColor.fromAHSV(1.0, 300.0, hsvColor.saturation, 1.0).toColor(), + HSVColor.fromAHSV(1.0, 360.0, hsvColor.saturation, 1.0).toColor(), + ]; + final Gradient gradientH = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradientH.createShader(rect)); + canvas.drawRect(rect, Paint()..shader = gradientV.createShader(rect)); + + canvas.drawCircle( + Offset( + size.width * hsvColor.hue / 360, + size.height * (1 - hsvColor.value), + ), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(hsvColor.toColor()) + ? Colors.white + : Colors.black) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for HS mixture. +class HSVWithValueColorPainter extends CustomPainter { + const HSVWithValueColorPainter(this.hsvColor, {this.pointerColor}); + + final HSVColor hsvColor; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + const Gradient gradientV = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.white], + ); + final List colors = [ + const HSVColor.fromAHSV(1.0, 0.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 60.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 120.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 180.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 240.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 300.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 360.0, 1.0, 1.0).toColor(), + ]; + final Gradient gradientH = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradientH.createShader(rect)); + canvas.drawRect(rect, Paint()..shader = gradientV.createShader(rect)); + canvas.drawRect( + rect, + Paint()..color = Colors.black.withOpacity(1 - hsvColor.value), + ); + + canvas.drawCircle( + Offset( + size.width * hsvColor.hue / 360, + size.height * (1 - hsvColor.saturation), + ), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(hsvColor.toColor()) + ? Colors.white + : Colors.black) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for SL mixture. +class HSLWithHueColorPainter extends CustomPainter { + const HSLWithHueColorPainter(this.hslColor, {this.pointerColor}); + + final HSLColor hslColor; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + final Gradient gradientH = LinearGradient( + colors: [ + const Color(0xff808080), + HSLColor.fromAHSL(1.0, hslColor.hue, 1.0, 0.5).toColor(), + ], + ); + const Gradient gradientV = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: [0.0, 0.5, 0.5, 1], + colors: [ + Colors.white, + Color(0x00ffffff), + Colors.transparent, + Colors.black, + ], + ); + canvas.drawRect(rect, Paint()..shader = gradientH.createShader(rect)); + canvas.drawRect(rect, Paint()..shader = gradientV.createShader(rect)); + + canvas.drawCircle( + Offset(size.width * hslColor.saturation, + size.height * (1 - hslColor.lightness)), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(hslColor.toColor()) + ? Colors.white + : Colors.black) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for HL mixture. +class HSLWithSaturationColorPainter extends CustomPainter { + const HSLWithSaturationColorPainter(this.hslColor, {this.pointerColor}); + + final HSLColor hslColor; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + final List colors = [ + HSLColor.fromAHSL(1.0, 0.0, hslColor.saturation, 0.5).toColor(), + HSLColor.fromAHSL(1.0, 60.0, hslColor.saturation, 0.5).toColor(), + HSLColor.fromAHSL(1.0, 120.0, hslColor.saturation, 0.5).toColor(), + HSLColor.fromAHSL(1.0, 180.0, hslColor.saturation, 0.5).toColor(), + HSLColor.fromAHSL(1.0, 240.0, hslColor.saturation, 0.5).toColor(), + HSLColor.fromAHSL(1.0, 300.0, hslColor.saturation, 0.5).toColor(), + HSLColor.fromAHSL(1.0, 360.0, hslColor.saturation, 0.5).toColor(), + ]; + final Gradient gradientH = LinearGradient(colors: colors); + const Gradient gradientV = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: [0.0, 0.5, 0.5, 1], + colors: [ + Colors.white, + Color(0x00ffffff), + Colors.transparent, + Colors.black, + ], + ); + canvas.drawRect(rect, Paint()..shader = gradientH.createShader(rect)); + canvas.drawRect(rect, Paint()..shader = gradientV.createShader(rect)); + + canvas.drawCircle( + Offset(size.width * hslColor.hue / 360, + size.height * (1 - hslColor.lightness)), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(hslColor.toColor()) + ? Colors.white + : Colors.black) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for HS mixture. +class HSLWithLightnessColorPainter extends CustomPainter { + const HSLWithLightnessColorPainter(this.hslColor, {this.pointerColor}); + + final HSLColor hslColor; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + final List colors = [ + const HSLColor.fromAHSL(1.0, 0.0, 1.0, 0.5).toColor(), + const HSLColor.fromAHSL(1.0, 60.0, 1.0, 0.5).toColor(), + const HSLColor.fromAHSL(1.0, 120.0, 1.0, 0.5).toColor(), + const HSLColor.fromAHSL(1.0, 180.0, 1.0, 0.5).toColor(), + const HSLColor.fromAHSL(1.0, 240.0, 1.0, 0.5).toColor(), + const HSLColor.fromAHSL(1.0, 300.0, 1.0, 0.5).toColor(), + const HSLColor.fromAHSL(1.0, 360.0, 1.0, 0.5).toColor(), + ]; + final Gradient gradientH = LinearGradient(colors: colors); + const Gradient gradientV = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Color(0xFF808080), + ], + ); + canvas.drawRect(rect, Paint()..shader = gradientH.createShader(rect)); + canvas.drawRect(rect, Paint()..shader = gradientV.createShader(rect)); + canvas.drawRect( + rect, + Paint() + ..color = + Colors.black.withOpacity((1 - hslColor.lightness * 2).clamp(0, 1)), + ); + canvas.drawRect( + rect, + Paint() + ..color = Colors.white + .withOpacity(((hslColor.lightness - 0.5) * 2).clamp(0, 1)), + ); + + canvas.drawCircle( + Offset(size.width * hslColor.hue / 360, + size.height * (1 - hslColor.saturation)), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(hslColor.toColor()) + ? Colors.white + : Colors.black) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for GB mixture. +class RGBWithRedColorPainter extends CustomPainter { + const RGBWithRedColorPainter(this.color, {this.pointerColor}); + + final Color color; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + final Gradient gradientH = LinearGradient( + colors: [ + Color.fromRGBO(color.red, 255, 0, 1.0), + Color.fromRGBO(color.red, 255, 255, 1.0), + ], + ); + final Gradient gradientV = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromRGBO(color.red, 255, 255, 1.0), + Color.fromRGBO(color.red, 0, 255, 1.0), + ], + ); + canvas.drawRect(rect, Paint()..shader = gradientH.createShader(rect)); + canvas.drawRect( + rect, + Paint() + ..shader = gradientV.createShader(rect) + ..blendMode = BlendMode.multiply, + ); + + canvas.drawCircle( + Offset( + size.width * color.blue / 255, size.height * (1 - color.green / 255)), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(color) ? Colors.white : Colors.black) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for RB mixture. +class RGBWithGreenColorPainter extends CustomPainter { + const RGBWithGreenColorPainter(this.color, {this.pointerColor}); + + final Color color; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + final Gradient gradientH = LinearGradient( + colors: [ + Color.fromRGBO(255, color.green, 0, 1.0), + Color.fromRGBO(255, color.green, 255, 1.0), + ], + ); + final Gradient gradientV = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromRGBO(255, color.green, 255, 1.0), + Color.fromRGBO(0, color.green, 255, 1.0), + ], + ); + canvas.drawRect(rect, Paint()..shader = gradientH.createShader(rect)); + canvas.drawRect( + rect, + Paint() + ..shader = gradientV.createShader(rect) + ..blendMode = BlendMode.multiply, + ); + + canvas.drawCircle( + Offset( + size.width * color.blue / 255, size.height * (1 - color.red / 255)), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(color) ? Colors.white : Colors.black) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for RG mixture. +class RGBWithBlueColorPainter extends CustomPainter { + const RGBWithBlueColorPainter(this.color, {this.pointerColor}); + + final Color color; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + final Gradient gradientH = LinearGradient( + colors: [ + Color.fromRGBO(0, 255, color.blue, 1.0), + Color.fromRGBO(255, 255, color.blue, 1.0), + ], + ); + final Gradient gradientV = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromRGBO(255, 255, color.blue, 1.0), + Color.fromRGBO(255, 0, color.blue, 1.0), + ], + ); + canvas.drawRect(rect, Paint()..shader = gradientH.createShader(rect)); + canvas.drawRect( + rect, + Paint() + ..shader = gradientV.createShader(rect) + ..blendMode = BlendMode.multiply, + ); + + canvas.drawCircle( + Offset( + size.width * color.red / 255, size.height * (1 - color.green / 255)), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(color) ? Colors.white : Colors.black) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for hue color wheel. +class HUEColorWheelPainter extends CustomPainter { + const HUEColorWheelPainter(this.hsvColor, {this.pointerColor}); + + final HSVColor hsvColor; + final Color? pointerColor; + + @override + void paint(Canvas canvas, Size size) { + Rect rect = Offset.zero & size; + Offset center = Offset(size.width / 2, size.height / 2); + double radio = size.width <= size.height ? size.width / 2 : size.height / 2; + + final List colors = [ + const HSVColor.fromAHSV(1.0, 360.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 300.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 240.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 180.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 120.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 60.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 0.0, 1.0, 1.0).toColor(), + ]; + final Gradient gradientS = SweepGradient(colors: colors); + const Gradient gradientR = RadialGradient( + colors: [ + Colors.white, + Color(0x00FFFFFF), + ], + ); + canvas.drawCircle( + center, radio, Paint()..shader = gradientS.createShader(rect)); + canvas.drawCircle( + center, radio, Paint()..shader = gradientR.createShader(rect)); + canvas.drawCircle(center, radio, + Paint()..color = Colors.black.withOpacity(1 - hsvColor.value)); + + canvas.drawCircle( + Offset( + center.dx + + hsvColor.saturation * radio * cos((hsvColor.hue * pi / 180)), + center.dy - + hsvColor.saturation * radio * sin((hsvColor.hue * pi / 180)), + ), + size.height * 0.04, + Paint() + ..color = pointerColor ?? + (useWhiteForeground(hsvColor.toColor()) + ? Colors.white + : Colors.black) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for hue ring. +class HueRingPainter extends CustomPainter { + const HueRingPainter(this.hsvColor, + {this.displayThumbColor = true, this.strokeWidth = 5}); + + final HSVColor hsvColor; + final bool displayThumbColor; + final double strokeWidth; + + @override + void paint(Canvas canvas, Size size) { + Rect rect = Offset.zero & size; + Offset center = Offset(size.width / 2, size.height / 2); + double radio = size.width <= size.height ? size.width / 2 : size.height / 2; + + final List colors = [ + const HSVColor.fromAHSV(1.0, 360.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 300.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 240.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 180.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 120.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 60.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 0.0, 1.0, 1.0).toColor(), + ]; + canvas.drawCircle( + center, + radio, + Paint() + ..shader = SweepGradient(colors: colors).createShader(rect) + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth, + ); + + final Offset offset = Offset( + center.dx + radio * cos((hsvColor.hue * pi / 180)), + center.dy - radio * sin((hsvColor.hue * pi / 180)), + ); + canvas.drawShadow( + Path()..addOval(Rect.fromCircle(center: offset, radius: 12)), + Colors.black, + 3.0, + true); + canvas.drawCircle( + offset, + size.height * 0.04, + Paint() + ..color = Colors.white + ..style = PaintingStyle.fill, + ); + if (displayThumbColor) { + canvas.drawCircle( + offset, + size.height * 0.03, + Paint() + ..color = hsvColor.toColor() + ..style = PaintingStyle.fill, + ); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +class _SliderLayout extends MultiChildLayoutDelegate { + static const String track = 'track'; + static const String thumb = 'thumb'; + static const String gestureContainer = 'gesturecontainer'; + + @override + void performLayout(Size size) { + layoutChild( + track, + BoxConstraints.tightFor( + width: size.width - 30.0, + height: size.height / 5, + ), + ); + positionChild(track, Offset(15.0, size.height * 0.4)); + layoutChild( + thumb, + BoxConstraints.tightFor(width: 5.0, height: size.height / 4), + ); + positionChild(thumb, Offset(0.0, size.height * 0.4)); + layoutChild( + gestureContainer, + BoxConstraints.tightFor(width: size.width, height: size.height), + ); + positionChild(gestureContainer, Offset.zero); + } + + @override + bool shouldRelayout(_SliderLayout oldDelegate) => false; +} + +/// Painter for all kinds of track types. +class TrackPainter extends CustomPainter { + const TrackPainter(this.trackType, this.hsvColor); + + final TrackType trackType; + final HSVColor hsvColor; + + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + if (trackType == TrackType.alpha) { + final Size chessSize = Size(size.height / 2, size.height / 2); + Paint chessPaintB = Paint()..color = const Color(0xffcccccc); + Paint chessPaintW = Paint()..color = Colors.white; + List.generate((size.height / chessSize.height).round(), (int y) { + List.generate((size.width / chessSize.width).round(), (int x) { + canvas.drawRect( + Offset(chessSize.width * x, chessSize.width * y) & chessSize, + (x + y) % 2 != 0 ? chessPaintW : chessPaintB, + ); + }); + }); + } + + switch (trackType) { + case TrackType.hue: + final List colors = [ + const HSVColor.fromAHSV(1.0, 0.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 60.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 120.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 180.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 240.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 300.0, 1.0, 1.0).toColor(), + const HSVColor.fromAHSV(1.0, 360.0, 1.0, 1.0).toColor(), + ]; + Gradient gradient = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + break; + case TrackType.saturation: + final List colors = [ + HSVColor.fromAHSV(1.0, hsvColor.hue, 0.0, 1.0).toColor(), + HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(), + ]; + Gradient gradient = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + break; + case TrackType.saturationForHSL: + final List colors = [ + HSLColor.fromAHSL(1.0, hsvColor.hue, 0.0, 0.5).toColor(), + HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 0.5).toColor(), + ]; + Gradient gradient = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + break; + case TrackType.value: + final List colors = [ + HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 0.0).toColor(), + HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(), + ]; + Gradient gradient = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + break; + case TrackType.lightness: + final List colors = [ + HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 0.0).toColor(), + HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 0.5).toColor(), + HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 1.0).toColor(), + ]; + Gradient gradient = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + break; + case TrackType.red: + final List colors = [ + hsvColor.toColor().withRed(0).withOpacity(1.0), + hsvColor.toColor().withRed(255).withOpacity(1.0), + ]; + Gradient gradient = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + break; + case TrackType.green: + final List colors = [ + hsvColor.toColor().withGreen(0).withOpacity(1.0), + hsvColor.toColor().withGreen(255).withOpacity(1.0), + ]; + Gradient gradient = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + break; + case TrackType.blue: + final List colors = [ + hsvColor.toColor().withBlue(0).withOpacity(1.0), + hsvColor.toColor().withBlue(255).withOpacity(1.0), + ]; + Gradient gradient = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + break; + case TrackType.alpha: + final List colors = [ + hsvColor.toColor().withOpacity(0.0), + hsvColor.toColor().withOpacity(1.0), + ]; + Gradient gradient = LinearGradient(colors: colors); + canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect)); + break; + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for thumb of slider. +class ThumbPainter extends CustomPainter { + const ThumbPainter({this.thumbColor, this.fullThumbColor = false}); + + final Color? thumbColor; + final bool fullThumbColor; + + @override + void paint(Canvas canvas, Size size) { + canvas.drawShadow( + Path() + ..addOval( + Rect.fromCircle( + center: const Offset(0.5, 2.0), radius: size.width * 1.8), + ), + Colors.black, + 3.0, + true, + ); + canvas.drawCircle( + Offset(0.0, size.height * 0.4), + size.height, + Paint() + ..color = Colors.white + ..style = PaintingStyle.fill); + if (thumbColor != null) { + canvas.drawCircle( + Offset(0.0, size.height * 0.4), + size.height * (fullThumbColor ? 1.0 : 0.65), + Paint() + ..color = thumbColor! + ..style = PaintingStyle.fill); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for chess type alpha background in color indicator widget. +class IndicatorPainter extends CustomPainter { + const IndicatorPainter(this.color); + + final Color color; + + @override + void paint(Canvas canvas, Size size) { + final Size chessSize = Size(size.width / 10, size.height / 10); + final Paint chessPaintB = Paint()..color = const Color(0xFFCCCCCC); + final Paint chessPaintW = Paint()..color = Colors.white; + List.generate((size.height / chessSize.height).round(), (int y) { + List.generate((size.width / chessSize.width).round(), (int x) { + canvas.drawRect( + Offset(chessSize.width * x, chessSize.height * y) & chessSize, + (x + y) % 2 != 0 ? chessPaintW : chessPaintB, + ); + }); + }); + + canvas.drawCircle( + Offset(size.width / 2, size.height / 2), + size.height / 2, + Paint() + ..color = color + ..style = PaintingStyle.fill); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Painter for chess type alpha background in slider track widget. +class CheckerPainter extends CustomPainter { + const CheckerPainter(); + + @override + void paint(Canvas canvas, Size size) { + final Size chessSize = Size(size.height / 6, size.height / 6); + Paint chessPaintB = Paint()..color = const Color(0xffcccccc); + Paint chessPaintW = Paint()..color = Colors.white; + List.generate((size.height / chessSize.height).round(), (int y) { + List.generate((size.width / chessSize.width).round(), (int x) { + canvas.drawRect( + Offset(chessSize.width * x, chessSize.width * y) & chessSize, + (x + y) % 2 != 0 ? chessPaintW : chessPaintB, + ); + }); + }); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +/// Provide label for color information. +class ColorPickerLabel extends StatefulWidget { + const ColorPickerLabel( + this.hsvColor, { + Key? key, + this.enableAlpha = true, + this.colorLabelTypes = const [ + ColorLabelType.rgb, + ColorLabelType.hsv, + ColorLabelType.hsl + ], + this.textStyle, + }) : assert(colorLabelTypes.length > 0), + super(key: key); + + final HSVColor hsvColor; + final bool enableAlpha; + final TextStyle? textStyle; + final List colorLabelTypes; + + @override + _ColorPickerLabelState createState() => _ColorPickerLabelState(); +} + +class _ColorPickerLabelState extends State { + final Map> _colorTypes = const { + ColorLabelType.hex: ['R', 'G', 'B', 'A'], + ColorLabelType.rgb: ['R', 'G', 'B', 'A'], + ColorLabelType.hsv: ['H', 'S', 'V', 'A'], + ColorLabelType.hsl: ['H', 'S', 'L', 'A'], + }; + + late ColorLabelType _colorType; + + @override + void initState() { + super.initState(); + _colorType = widget.colorLabelTypes[0]; + } + + List colorValue(HSVColor hsvColor, ColorLabelType colorLabelType) { + if (colorLabelType == ColorLabelType.hex) { + final Color color = hsvColor.toColor(); + return [ + color.red.toRadixString(16).toUpperCase().padLeft(2, '0'), + color.green.toRadixString(16).toUpperCase().padLeft(2, '0'), + color.blue.toRadixString(16).toUpperCase().padLeft(2, '0'), + color.alpha.toRadixString(16).toUpperCase().padLeft(2, '0'), + ]; + } else if (colorLabelType == ColorLabelType.rgb) { + final Color color = hsvColor.toColor(); + return [ + color.red.toString(), + color.green.toString(), + color.blue.toString(), + '${(color.opacity * 100).round()}%', + ]; + } else if (colorLabelType == ColorLabelType.hsv) { + return [ + '${hsvColor.hue.round()}°', + '${(hsvColor.saturation * 100).round()}%', + '${(hsvColor.value * 100).round()}%', + '${(hsvColor.alpha * 100).round()}%', + ]; + } else if (colorLabelType == ColorLabelType.hsl) { + HSLColor hslColor = hsvToHsl(hsvColor); + return [ + '${hslColor.hue.round()}°', + '${(hslColor.saturation * 100).round()}%', + '${(hslColor.lightness * 100).round()}%', + '${(hsvColor.alpha * 100).round()}%', + ]; + } else { + return ['??', '??', '??', '??']; + } + } + + List colorValueLabels() { + double fontSize = 14; + if (widget.textStyle != null && widget.textStyle?.fontSize != null) + fontSize = widget.textStyle?.fontSize ?? 14; + + return [ + for (String item in _colorTypes[_colorType] ?? []) + if (widget.enableAlpha || item != 'A') + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: fontSize * 2), + child: IntrinsicHeight( + child: Column( + children: [ + Text( + item, + style: widget.textStyle ?? + Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 10.0), + Expanded( + child: Text( + colorValue(widget.hsvColor, _colorType)[ + _colorTypes[_colorType]!.indexOf(item)], + overflow: TextOverflow.ellipsis, + style: widget.textStyle ?? + Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + ), + ), + ) + ]; + } + + @override + Widget build(BuildContext context) { + return Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + DropdownButton( + value: _colorType, + onChanged: (ColorLabelType? type) { + if (type != null) setState(() => _colorType = type); + }, + items: [ + for (ColorLabelType type in widget.colorLabelTypes) + DropdownMenuItem( + value: type, + child: Text(type.toString().split('.').last.toUpperCase()), + ) + ], + ), + const SizedBox(width: 10.0), + ...colorValueLabels(), + ]); + } +} + +/// Provide hex input wiget for 3/6/8 digits. +class ColorPickerInput extends StatefulWidget { + const ColorPickerInput( + this.color, + this.onColorChanged, { + Key? key, + this.enableAlpha = true, + this.embeddedText = false, + this.disable = false, + }) : super(key: key); + + final Color color; + final ValueChanged onColorChanged; + final bool enableAlpha; + final bool embeddedText; + final bool disable; + + @override + _ColorPickerInputState createState() => _ColorPickerInputState(); +} + +class _ColorPickerInputState extends State { + TextEditingController textEditingController = TextEditingController(); + int inputColor = 0; + + @override + void dispose() { + textEditingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (inputColor != widget.color.value) { + textEditingController.text = '#' + + widget.color.red.toRadixString(16).toUpperCase().padLeft(2, '0') + + widget.color.green.toRadixString(16).toUpperCase().padLeft(2, '0') + + widget.color.blue.toRadixString(16).toUpperCase().padLeft(2, '0') + + (widget.enableAlpha + ? widget.color.alpha + .toRadixString(16) + .toUpperCase() + .padLeft(2, '0') + : ''); + } + return Padding( + padding: const EdgeInsets.only(top: 5.0), + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + if (!widget.embeddedText) + Text('Hex', style: Theme.of(context).textTheme.bodyLarge), + const SizedBox(width: 10), + SizedBox( + width: (Theme.of(context).textTheme.bodyMedium?.fontSize ?? 14) * 10, + child: TextField( + enabled: !widget.disable, + controller: textEditingController, + inputFormatters: [ + UpperCaseTextFormatter(), + FilteringTextInputFormatter.allow(RegExp(kValidHexPattern)), + ], + decoration: InputDecoration( + isDense: true, + label: widget.embeddedText ? const Text('Hex') : null, + contentPadding: const EdgeInsets.symmetric(vertical: 5), + ), + onChanged: (String value) { + String input = value; + if (value.length == 9) { + input = value.split('').getRange(7, 9).join() + + value.split('').getRange(1, 7).join(); + } + final Color? color = colorFromHex(input); + if (color != null) { + widget.onColorChanged(color); + inputColor = color.value; + } + }, + ), + ), + ]), + ); + } +} + +/// 9 track types for slider picker widget. +class ColorPickerSlider extends StatelessWidget { + const ColorPickerSlider( + this.trackType, + this.hsvColor, + this.onColorChanged, { + Key? key, + this.displayThumbColor = false, + this.fullThumbColor = false, + }) : super(key: key); + + final TrackType trackType; + final HSVColor hsvColor; + final ValueChanged onColorChanged; + final bool displayThumbColor; + final bool fullThumbColor; + + void slideEvent(RenderBox getBox, BoxConstraints box, Offset globalPosition) { + double localDx = getBox.globalToLocal(globalPosition).dx - 15.0; + double progress = + localDx.clamp(0.0, box.maxWidth - 30.0) / (box.maxWidth - 30.0); + switch (trackType) { + case TrackType.hue: + // 360 is the same as zero + // if set to 360, sliding to end goes to zero + onColorChanged(hsvColor.withHue(progress * 359)); + break; + case TrackType.saturation: + onColorChanged(hsvColor.withSaturation(progress)); + break; + case TrackType.saturationForHSL: + onColorChanged(hslToHsv(hsvToHsl(hsvColor).withSaturation(progress))); + break; + case TrackType.value: + onColorChanged(hsvColor.withValue(progress)); + break; + case TrackType.lightness: + onColorChanged(hslToHsv(hsvToHsl(hsvColor).withLightness(progress))); + break; + case TrackType.red: + onColorChanged(HSVColor.fromColor( + hsvColor.toColor().withRed((progress * 0xff).round()))); + break; + case TrackType.green: + onColorChanged(HSVColor.fromColor( + hsvColor.toColor().withGreen((progress * 0xff).round()))); + break; + case TrackType.blue: + onColorChanged(HSVColor.fromColor( + hsvColor.toColor().withBlue((progress * 0xff).round()))); + break; + case TrackType.alpha: + onColorChanged(hsvColor.withAlpha( + localDx.clamp(0.0, box.maxWidth - 30.0) / (box.maxWidth - 30.0))); + break; + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (BuildContext context, BoxConstraints box) { + double thumbOffset = 15.0; + Color thumbColor; + switch (trackType) { + case TrackType.hue: + thumbOffset += (box.maxWidth - 30.0) * hsvColor.hue / 360; + thumbColor = HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(); + break; + case TrackType.saturation: + thumbOffset += (box.maxWidth - 30.0) * hsvColor.saturation; + thumbColor = + HSVColor.fromAHSV(1.0, hsvColor.hue, hsvColor.saturation, 1.0) + .toColor(); + break; + case TrackType.saturationForHSL: + thumbOffset += (box.maxWidth - 30.0) * hsvToHsl(hsvColor).saturation; + thumbColor = HSLColor.fromAHSL( + 1.0, hsvColor.hue, hsvToHsl(hsvColor).saturation, 0.5) + .toColor(); + break; + case TrackType.value: + thumbOffset += (box.maxWidth - 30.0) * hsvColor.value; + thumbColor = HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, hsvColor.value) + .toColor(); + break; + case TrackType.lightness: + thumbOffset += (box.maxWidth - 30.0) * hsvToHsl(hsvColor).lightness; + thumbColor = HSLColor.fromAHSL( + 1.0, hsvColor.hue, 1.0, hsvToHsl(hsvColor).lightness) + .toColor(); + break; + case TrackType.red: + thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().red / 0xff; + thumbColor = hsvColor.toColor().withOpacity(1.0); + break; + case TrackType.green: + thumbOffset += + (box.maxWidth - 30.0) * hsvColor.toColor().green / 0xff; + thumbColor = hsvColor.toColor().withOpacity(1.0); + break; + case TrackType.blue: + thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().blue / 0xff; + thumbColor = hsvColor.toColor().withOpacity(1.0); + break; + case TrackType.alpha: + thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().opacity; + thumbColor = hsvColor.toColor().withOpacity(hsvColor.alpha); + break; + } + + return CustomMultiChildLayout( + delegate: _SliderLayout(), + children: [ + LayoutId( + id: _SliderLayout.track, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(50.0)), + child: CustomPaint( + painter: TrackPainter( + trackType, + hsvColor, + )), + ), + ), + LayoutId( + id: _SliderLayout.thumb, + child: Transform.translate( + offset: Offset(thumbOffset, 0.0), + child: CustomPaint( + painter: ThumbPainter( + thumbColor: displayThumbColor ? thumbColor : null, + fullThumbColor: fullThumbColor, + ), + ), + ), + ), + LayoutId( + id: _SliderLayout.gestureContainer, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints box) { + RenderBox? getBox = context.findRenderObject() as RenderBox?; + return GestureDetector( + onPanDown: (DragDownDetails details) => getBox != null + ? slideEvent(getBox, box, details.globalPosition) + : null, + onPanUpdate: (DragUpdateDetails details) => getBox != null + ? slideEvent(getBox, box, details.globalPosition) + : null, + ); + }, + ), + ), + ], + ); + }); + } +} + +/// Simple round color indicator. +class ColorIndicator extends StatelessWidget { + const ColorIndicator( + this.hsvColor, { + Key? key, + this.width = 50.0, + this.height = 50.0, + }) : super(key: key); + + final HSVColor hsvColor; + final double width; + final double height; + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(1000.0)), + border: Border.all(color: const Color(0xffdddddd)), + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(1000.0)), + child: CustomPaint(painter: IndicatorPainter(hsvColor.toColor())), + ), + ); + } +} + +/// Provide Rectangle & Circle 2 categories, 10 variations of palette widget. +class ColorPickerArea extends StatelessWidget { + const ColorPickerArea( + this.hsvColor, + this.onColorChanged, + this.paletteType, { + Key? key, + }) : super(key: key); + + final HSVColor hsvColor; + final ValueChanged onColorChanged; + final PaletteType paletteType; + + void _handleColorRectChange(double horizontal, double vertical) { + switch (paletteType) { + case PaletteType.hsv: + case PaletteType.hsvWithHue: + onColorChanged(hsvColor.withSaturation(horizontal).withValue(vertical)); + break; + case PaletteType.hsvWithSaturation: + onColorChanged(hsvColor.withHue(horizontal * 360).withValue(vertical)); + break; + case PaletteType.hsvWithValue: + onColorChanged( + hsvColor.withHue(horizontal * 360).withSaturation(vertical)); + break; + case PaletteType.hsl: + case PaletteType.hslWithHue: + onColorChanged(hslToHsv( + hsvToHsl(hsvColor).withSaturation(horizontal).withLightness(vertical), + )); + break; + case PaletteType.hslWithSaturation: + onColorChanged(hslToHsv( + hsvToHsl(hsvColor).withHue(horizontal * 360).withLightness(vertical), + )); + break; + case PaletteType.hslWithLightness: + onColorChanged(hslToHsv( + hsvToHsl(hsvColor).withHue(horizontal * 360).withSaturation(vertical), + )); + break; + case PaletteType.rgbWithRed: + onColorChanged(HSVColor.fromColor( + hsvColor + .toColor() + .withBlue((horizontal * 255).round()) + .withGreen((vertical * 255).round()), + )); + break; + case PaletteType.rgbWithGreen: + onColorChanged(HSVColor.fromColor( + hsvColor + .toColor() + .withBlue((horizontal * 255).round()) + .withRed((vertical * 255).round()), + )); + break; + case PaletteType.rgbWithBlue: + onColorChanged(HSVColor.fromColor( + hsvColor + .toColor() + .withRed((horizontal * 255).round()) + .withGreen((vertical * 255).round()), + )); + break; + default: + break; + } + } + + void _handleColorWheelChange(double hue, double radio) { + onColorChanged(hsvColor.withHue(hue).withSaturation(radio)); + } + + void _handleGesture( + Offset position, BuildContext context, double height, double width) { + RenderBox? getBox = context.findRenderObject() as RenderBox?; + if (getBox == null) return; + + Offset localOffset = getBox.globalToLocal(position); + double horizontal = localOffset.dx.clamp(0.0, width); + double vertical = localOffset.dy.clamp(0.0, height); + + if (paletteType == PaletteType.hueWheel) { + Offset center = Offset(width / 2, height / 2); + double radio = width <= height ? width / 2 : height / 2; + double dist = + sqrt(pow(horizontal - center.dx, 2) + pow(vertical - center.dy, 2)) / + radio; + double rad = + (atan2(horizontal - center.dx, vertical - center.dy) / pi + 1) / + 2 * + 360; + _handleColorWheelChange( + ((rad + 90) % 360).clamp(0, 360), dist.clamp(0, 1)); + } else { + _handleColorRectChange(horizontal / width, 1 - vertical / height); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + double width = constraints.maxWidth; + double height = constraints.maxHeight; + + return RawGestureDetector( + gestures: { + _AlwaysWinPanGestureRecognizer: + GestureRecognizerFactoryWithHandlers< + _AlwaysWinPanGestureRecognizer>( + () => _AlwaysWinPanGestureRecognizer(), + (_AlwaysWinPanGestureRecognizer instance) { + instance + ..onDown = ((details) => _handleGesture( + details.globalPosition, context, height, width)) + ..onUpdate = ((details) => _handleGesture( + details.globalPosition, context, height, width)); + }, + ), + }, + child: Builder( + builder: (BuildContext _) { + switch (paletteType) { + case PaletteType.hsv: + case PaletteType.hsvWithHue: + return CustomPaint(painter: HSVWithHueColorPainter(hsvColor)); + case PaletteType.hsvWithSaturation: + return CustomPaint( + painter: HSVWithSaturationColorPainter(hsvColor)); + case PaletteType.hsvWithValue: + return CustomPaint( + painter: HSVWithValueColorPainter(hsvColor)); + case PaletteType.hsl: + case PaletteType.hslWithHue: + return CustomPaint( + painter: HSLWithHueColorPainter(hsvToHsl(hsvColor))); + case PaletteType.hslWithSaturation: + return CustomPaint( + painter: + HSLWithSaturationColorPainter(hsvToHsl(hsvColor))); + case PaletteType.hslWithLightness: + return CustomPaint( + painter: + HSLWithLightnessColorPainter(hsvToHsl(hsvColor))); + case PaletteType.rgbWithRed: + return CustomPaint( + painter: RGBWithRedColorPainter(hsvColor.toColor())); + case PaletteType.rgbWithGreen: + return CustomPaint( + painter: RGBWithGreenColorPainter(hsvColor.toColor())); + case PaletteType.rgbWithBlue: + return CustomPaint( + painter: RGBWithBlueColorPainter(hsvColor.toColor())); + case PaletteType.hueWheel: + return CustomPaint(painter: HUEColorWheelPainter(hsvColor)); + default: + return const CustomPaint(); + } + }, + ), + ); + }, + ); + } +} + +/// Provide Hue Ring with HSV Rectangle of palette widget. +class ColorPickerHueRing extends StatelessWidget { + const ColorPickerHueRing( + this.hsvColor, + this.onColorChanged, { + Key? key, + this.displayThumbColor = true, + this.strokeWidth = 5.0, + }) : super(key: key); + + final HSVColor hsvColor; + final ValueChanged onColorChanged; + final bool displayThumbColor; + final double strokeWidth; + + void _handleGesture( + Offset position, BuildContext context, double height, double width) { + RenderBox? getBox = context.findRenderObject() as RenderBox?; + if (getBox == null) return; + + Offset localOffset = getBox.globalToLocal(position); + double horizontal = localOffset.dx.clamp(0.0, width); + double vertical = localOffset.dy.clamp(0.0, height); + + Offset center = Offset(width / 2, height / 2); + double radio = width <= height ? width / 2 : height / 2; + double dist = + sqrt(pow(horizontal - center.dx, 2) + pow(vertical - center.dy, 2)) / + radio; + double rad = + (atan2(horizontal - center.dx, vertical - center.dy) / pi + 1) / + 2 * + 360; + if (dist > 0.7 && dist < 1.3) + onColorChanged(hsvColor.withHue(((rad + 90) % 360).clamp(0, 360))); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + double width = constraints.maxWidth; + double height = constraints.maxHeight; + + return RawGestureDetector( + gestures: { + _AlwaysWinPanGestureRecognizer: + GestureRecognizerFactoryWithHandlers< + _AlwaysWinPanGestureRecognizer>( + () => _AlwaysWinPanGestureRecognizer(), + (_AlwaysWinPanGestureRecognizer instance) { + instance + ..onDown = ((details) => _handleGesture( + details.globalPosition, context, height, width)) + ..onUpdate = ((details) => _handleGesture( + details.globalPosition, context, height, width)); + }, + ), + }, + child: CustomPaint( + painter: HueRingPainter(hsvColor, + displayThumbColor: displayThumbColor, strokeWidth: strokeWidth), + ), + ); + }, + ); + } +} + +class _AlwaysWinPanGestureRecognizer extends PanGestureRecognizer { + @override + void addAllowedPointer(event) { + super.addAllowedPointer(event); + resolve(GestureDisposition.accepted); + } + + @override + String get debugDescription => 'alwaysWin'; +} + +/// Uppercase text formater +class UpperCaseTextFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate(oldValue, TextEditingValue newValue) => + TextEditingValue( + text: newValue.text.toUpperCase(), selection: newValue.selection); +} diff --git a/lib/src/packages/flutter_colorpicker/src/utils.dart b/lib/src/packages/flutter_colorpicker/src/utils.dart new file mode 100644 index 00000000..bba99353 --- /dev/null +++ b/lib/src/packages/flutter_colorpicker/src/utils.dart @@ -0,0 +1,224 @@ +// ignore_for_file: type=lint + +/// Common function lib + +import 'dart:math'; +import 'package:flutter/painting.dart'; +import 'colors.dart'; + +/// Check if is good condition to use white foreground color by passing +/// the background color, and optional bias. +/// +/// Reference: +/// +/// Old: https://www.w3.org/TR/WCAG20-TECHS/G18.html +/// +/// New: https://github.com/mchome/flutter_statusbarcolor/issues/40 +bool useWhiteForeground(Color backgroundColor, {double bias = 0.0}) { + // Old: + // return 1.05 / (color.computeLuminance() + 0.05) > 4.5; + + // New: + int v = sqrt(pow(backgroundColor.red, 2) * 0.299 + + pow(backgroundColor.green, 2) * 0.587 + + pow(backgroundColor.blue, 2) * 0.114) + .round(); + return v < 130 + bias ? true : false; +} + +/// Convert HSV to HSL +/// +/// Reference: https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL +HSLColor hsvToHsl(HSVColor color) { + double s = 0.0; + double l = 0.0; + l = (2 - color.saturation) * color.value / 2; + if (l != 0) { + if (l == 1) { + s = 0.0; + } else if (l < 0.5) { + s = color.saturation * color.value / (l * 2); + } else { + s = color.saturation * color.value / (2 - l * 2); + } + } + return HSLColor.fromAHSL( + color.alpha, + color.hue, + s.clamp(0.0, 1.0), + l.clamp(0.0, 1.0), + ); +} + +/// Convert HSL to HSV +/// +/// Reference: https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV +HSVColor hslToHsv(HSLColor color) { + double s = 0.0; + double v = 0.0; + + v = color.lightness + + color.saturation * + (color.lightness < 0.5 ? color.lightness : 1 - color.lightness); + if (v != 0) s = 2 - 2 * color.lightness / v; + + return HSVColor.fromAHSV( + color.alpha, + color.hue, + s.clamp(0.0, 1.0), + v.clamp(0.0, 1.0), + ); +} + +/// [RegExp] pattern for validation HEX color [String] inputs, allows only: +/// +/// * exactly 1 to 8 digits in HEX format, +/// * only Latin A-F characters, case insensitive, +/// * and integer numbers 0,1,2,3,4,5,6,7,8,9, +/// * with optional hash (`#`) symbol at the beginning (not calculated in length). +/// +/// ```dart +/// final RegExp hexInputValidator = RegExp(kValidHexPattern); +/// if (hexInputValidator.hasMatch(hex)) print('$hex might be a valid HEX color'); +/// ``` +/// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet +const String kValidHexPattern = r'^#?[0-9a-fA-F]{1,8}'; + +/// [RegExp] pattern for validation complete HEX color [String], allows only: +/// +/// * exactly 6 or 8 digits in HEX format, +/// * only Latin A-F characters, case insensitive, +/// * and integer numbers 0,1,2,3,4,5,6,7,8,9, +/// * with optional hash (`#`) symbol at the beginning (not calculated in length). +/// +/// ```dart +/// final RegExp hexCompleteValidator = RegExp(kCompleteValidHexPattern); +/// if (hexCompleteValidator.hasMatch(hex)) print('$hex is valid HEX color'); +/// ``` +/// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet +const String kCompleteValidHexPattern = + r'^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$'; + +/// Try to convert text input or any [String] to valid [Color]. +/// The [String] must be provided in one of those formats: +/// +/// * RGB +/// * #RGB +/// * RRGGBB +/// * #RRGGBB +/// * AARRGGBB +/// * #AARRGGBB +/// +/// Where: A stands for Alpha, R for Red, G for Green, and B for blue color. +/// It will only accept 3/6/8 long HEXs with an optional hash (`#`) at the beginning. +/// Allowed characters are Latin A-F case insensitive and numbers 0-9. +/// Optional [enableAlpha] can be provided (it's `true` by default). If it's set +/// to `false` transparency information (alpha channel) will be removed. +/// ```dart +/// /// // Valid 3 digit HEXs: +/// colorFromHex('abc') == Color(0xffaabbcc) +/// colorFromHex('ABc') == Color(0xffaabbcc) +/// colorFromHex('ABC') == Color(0xffaabbcc) +/// colorFromHex('#Abc') == Color(0xffaabbcc) +/// colorFromHex('#abc') == Color(0xffaabbcc) +/// colorFromHex('#ABC') == Color(0xffaabbcc) +/// // Valid 6 digit HEXs: +/// colorFromHex('aabbcc') == Color(0xffaabbcc) +/// colorFromHex('AABbcc') == Color(0xffaabbcc) +/// colorFromHex('AABBCC') == Color(0xffaabbcc) +/// colorFromHex('#AABbcc') == Color(0xffaabbcc) +/// colorFromHex('#aabbcc') == Color(0xffaabbcc) +/// colorFromHex('#AABBCC') == Color(0xffaabbcc) +/// // Valid 8 digit HEXs: +/// colorFromHex('ffaabbcc') == Color(0xffaabbcc) +/// colorFromHex('ffAABbcc') == Color(0xffaabbcc) +/// colorFromHex('ffAABBCC') == Color(0xffaabbcc) +/// colorFromHex('ffaabbcc', enableAlpha: true) == Color(0xffaabbcc) +/// colorFromHex('FFAAbbcc', enableAlpha: true) == Color(0xffaabbcc) +/// colorFromHex('ffAABBCC', enableAlpha: true) == Color(0xffaabbcc) +/// colorFromHex('FFaabbcc', enableAlpha: true) == Color(0xffaabbcc) +/// colorFromHex('#ffaabbcc') == Color(0xffaabbcc) +/// colorFromHex('#ffAABbcc') == Color(0xffaabbcc) +/// colorFromHex('#FFAABBCC') == Color(0xffaabbcc) +/// colorFromHex('#ffaabbcc', enableAlpha: true) == Color(0xffaabbcc) +/// colorFromHex('#FFAAbbcc', enableAlpha: true) == Color(0xffaabbcc) +/// colorFromHex('#ffAABBCC', enableAlpha: true) == Color(0xffaabbcc) +/// colorFromHex('#FFaabbcc', enableAlpha: true) == Color(0xffaabbcc) +/// // Invalid HEXs: +/// colorFromHex('bc') == null // length 2 +/// colorFromHex('aabbc') == null // length 5 +/// colorFromHex('#ffaabbccd') == null // length 9 (+#) +/// colorFromHex('aabbcx') == null // x character +/// colorFromHex('#aabbвв') == null // в non-latin character +/// colorFromHex('') == null // empty +/// ``` +/// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet +Color? colorFromHex(String inputString, {bool enableAlpha = true}) { + // Registers validator for exactly 6 or 8 digits long HEX (with optional #). + final RegExp hexValidator = RegExp(kCompleteValidHexPattern); + // Validating input, if it does not match — it's not proper HEX. + if (!hexValidator.hasMatch(inputString)) return null; + // Remove optional hash if exists and convert HEX to UPPER CASE. + String hexToParse = inputString.replaceFirst('#', '').toUpperCase(); + // It may allow HEXs with transparency information even if alpha is disabled, + if (!enableAlpha && hexToParse.length == 8) { + // but it will replace this info with 100% non-transparent value (FF). + hexToParse = 'FF${hexToParse.substring(2)}'; + } + // HEX may be provided in 3-digits format, let's just duplicate each letter. + if (hexToParse.length == 3) { + hexToParse = hexToParse.split('').expand((i) => [i * 2]).join(); + } + // We will need 8 digits to parse the color, let's add missing digits. + if (hexToParse.length == 6) hexToParse = 'FF$hexToParse'; + // HEX must be valid now, but as a precaution, it will just "try" to parse it. + final intColorValue = int.tryParse(hexToParse, radix: 16); + // If for some reason HEX is not valid — abort the operation, return nothing. + if (intColorValue == null) return null; + // Register output color for the last step. + final color = Color(intColorValue); + // Decide to return color with transparency information or not. + return enableAlpha ? color : color.withAlpha(255); +} + +/// Converts `dart:ui` [Color] to the 6/8 digits HEX [String]. +/// +/// Prefixes a hash (`#`) sign if [includeHashSign] is set to `true`. +/// The result will be provided as UPPER CASE, it can be changed via [toUpperCase] +/// flag set to `false` (default is `true`). Hex can be returned without alpha +/// channel information (transparency), with the [enableAlpha] flag set to `false`. +String colorToHex( + Color color, { + bool includeHashSign = false, + bool enableAlpha = true, + bool toUpperCase = true, +}) { + final String hex = (includeHashSign ? '#' : '') + + (enableAlpha ? _padRadix(color.alpha) : '') + + _padRadix(color.red) + + _padRadix(color.green) + + _padRadix(color.blue); + return toUpperCase ? hex.toUpperCase() : hex; +} + +// Shorthand for padLeft of RadixString, DRY. +String _padRadix(int value) => value.toRadixString(16).padLeft(2, '0'); + +// Extension for String +extension ColorExtension1 on String { + Color? toColor() { + Color? color = colorFromName(this); + if (color != null) return color; + return colorFromHex(this); + } +} + +// Extension from Color +extension ColorExtension2 on Color { + String toHexString( + {bool includeHashSign = false, + bool enableAlpha = true, + bool toUpperCase = true}) => + colorToHex(this, + includeHashSign: false, enableAlpha: true, toUpperCase: true); +} diff --git a/lib/src/widgets/toolbar/buttons/color/color_dialog.dart b/lib/src/widgets/toolbar/buttons/color/color_dialog.dart index e7d65800..fc336932 100644 --- a/lib/src/widgets/toolbar/buttons/color/color_dialog.dart +++ b/lib/src/widgets/toolbar/buttons/color/color_dialog.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart' - show ColorPicker, MaterialPicker, colorToHex; import '../../../../../translations.dart'; import '../../../../models/documents/style.dart'; +import '../../../../packages/flutter_colorpicker/flutter_colorpicker.dart' + show ColorPicker, MaterialPicker, colorToHex; import 'color_button.dart' show hexToColor; enum _PickerType { diff --git a/pubspec.yaml b/pubspec.yaml index 4eb9954d..ef83fbc4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,8 @@ dependencies: meta: ^1.10.0 html: ^0.15.4 - flutter_colorpicker: ^1.0.3 + # TODO: temporarily disable from https://pub.dev/packages/flutter_colorpicker and clone it in the `lib/src/packages/flutter_colorpicker` as it's hasn't been published on pub.dev for a while + # flutter_colorpicker: ^1.0.3 # For converting HTML to Quill delta markdown: ^7.2.1