diff --git a/feedback/example/android/app/build.gradle b/feedback/example/android/app/build.gradle
index fdc675a7..80026f6e 100644
--- a/feedback/example/android/app/build.gradle
+++ b/feedback/example/android/app/build.gradle
@@ -1,3 +1,9 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@@ -6,10 +12,10 @@ if (localPropertiesFile.exists()) {
}
}
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
- throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
+//def flutterRoot = localProperties.getProperty('flutter.sdk')
+//if (flutterRoot == null) {
+// throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+//}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
@@ -21,12 +27,12 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+//apply plugin: 'com.android.application'
+//apply plugin: 'kotlin-android'
+//apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
- compileSdkVersion 33
+ compileSdkVersion 34
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -37,9 +43,11 @@ android {
}
defaultConfig {
+ namespace "com.example.example"
applicationId "com.example.example"
- minSdkVersion 19
+ minSdkVersion flutter.minSdkVersion
targetSdkVersion 33
+ multiDexEnabled true
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -57,8 +65,9 @@ flutter {
}
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+// implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ implementation 'com.android.support:multidex:2.0.1'
}
diff --git a/feedback/example/android/build.gradle b/feedback/example/android/build.gradle
index bf7d9f68..5869be3d 100644
--- a/feedback/example/android/build.gradle
+++ b/feedback/example/android/build.gradle
@@ -1,16 +1,3 @@
-buildscript {
- ext.kotlin_version = '1.9.23'
- repositories {
- google()
- jcenter()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:7.4.2'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
-
allprojects {
repositories {
google()
diff --git a/feedback/example/android/settings.gradle b/feedback/example/android/settings.gradle
index 5a2f14fb..259b6ec0 100644
--- a/feedback/example/android/settings.gradle
+++ b/feedback/example/android/settings.gradle
@@ -1,15 +1,25 @@
-include ':app'
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }()
-def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
-def plugins = new Properties()
-def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
-if (pluginsFile.exists()) {
- pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
}
-plugins.each { name, path ->
- def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
- include ":$name"
- project(":$name").projectDir = pluginDirectory
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "8.1.0" apply false
+ id "org.jetbrains.kotlin.android" version "1.9.23" apply false
}
+
+include ":app"
\ No newline at end of file
diff --git a/feedback/example/ios/Flutter/AppFrameworkInfo.plist b/feedback/example/ios/Flutter/AppFrameworkInfo.plist
index f2872cf4..8c6e5614 100644
--- a/feedback/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/feedback/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 9.0
+ 12.0
diff --git a/feedback/example/ios/Podfile b/feedback/example/ios/Podfile
index 1e8c3c90..279576f3 100644
--- a/feedback/example/ios/Podfile
+++ b/feedback/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '9.0'
+# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/feedback/example/ios/Runner.xcodeproj/project.pbxproj b/feedback/example/ios/Runner.xcodeproj/project.pbxproj
index 74406c5c..6391ad31 100644
--- a/feedback/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/feedback/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 50;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -68,7 +68,6 @@
553502822E43C0A05ECA673C /* Pods-Runner.release.xcconfig */,
A4931B4FF5CA58F3689F1E8E /* Pods-Runner.profile.xcconfig */,
);
- name = Pods;
path = Pods;
sourceTree = "";
};
@@ -164,7 +163,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1300;
+ LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -214,14 +213,14 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/flutter_email_sender/flutter_email_sender.framework",
- "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework",
+ "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_email_sender.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
);
@@ -232,10 +231,12 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -246,6 +247,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -355,7 +357,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -371,6 +373,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = 94V52BVTWJ;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -385,7 +388,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+ PRODUCT_BUNDLE_IDENTIFIER = com.bashieralsaed.ladders;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -440,7 +443,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -489,7 +492,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -506,6 +509,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = 94V52BVTWJ;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -520,7 +524,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+ PRODUCT_BUNDLE_IDENTIFIER = com.bashieralsaed.ladders;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -536,6 +540,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = 94V52BVTWJ;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -550,7 +555,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+ PRODUCT_BUNDLE_IDENTIFIER = com.bashieralsaed.ladders;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
diff --git a/feedback/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/feedback/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 3db53b6e..e67b2808 100644
--- a/feedback/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/feedback/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
+ CADisableMinimumFrameDurationOnPhone
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
@@ -22,6 +24,8 @@
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
+ UIApplicationSupportsIndirectInputEvents
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
@@ -41,7 +45,5 @@
UIViewControllerBasedStatusBarAppearance
- CADisableMinimumFrameDurationOnPhone
-
diff --git a/feedback/lib/src/color_picker/alpha_slider.dart b/feedback/lib/src/color_picker/alpha_slider.dart
new file mode 100644
index 00000000..b07e611c
--- /dev/null
+++ b/feedback/lib/src/color_picker/alpha_slider.dart
@@ -0,0 +1,81 @@
+part of 'color_picker_icon.dart';
+
+class _AlphaSlider extends StatefulWidget {
+ const _AlphaSlider({
+ required this.color,
+ required this.whiteToColorValue,
+ required this.outsideSetter,
+ });
+
+ final Color color;
+ final double whiteToColorValue;
+ final ValueSetter outsideSetter;
+
+ @override
+ State<_AlphaSlider> createState() => _AlphaSliderState();
+}
+
+class _AlphaSliderState extends State<_AlphaSlider> {
+
+ Color get color => Color.lerp(_startColor, widget.color, widget.whiteToColorValue)!;
+ late var _sliderValue = _cachedAlphaSlider;
+
+ @override
+ Widget build(BuildContext context) => SizedBox(
+ height: _thumbRadius,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ Container(
+ height: _thumbRadius / 2,
+ margin: EdgeInsets.symmetric(horizontal: _thumbRadius),
+ decoration: ShapeDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ colors: [
+ color.withAlpha(_minAlpha),
+ color.withAlpha(_maxAlpha),
+ ],
+ ),
+ shape: StadiumBorder(),
+ ),
+ ),
+ Material(
+ color: Colors.transparent,
+ child: SliderTheme(
+ data: SliderThemeData(
+ thumbShape: const RoundSliderThumbShape(
+ enabledThumbRadius: _thumbRadius * 0.65,
+ ),
+ overlayShape: const RoundSliderOverlayShape(
+ overlayRadius: _thumbRadius,
+ ),
+ ),
+ child: Slider(
+ activeColor: Colors.transparent,
+ min: _minAlpha.toDouble(),
+ max: _maxAlpha.toDouble(),
+ inactiveColor: Colors.transparent,
+ thumbColor: color.withAlpha(
+ // We limit only the look of the thumb to the middle of [_maxAlpha]
+ // however the value in [_sliderValue] is still reflected properly
+ max(_maxAlpha / 2, _sliderValue).round(),
+ ),
+ value: _sliderValue,
+ onChanged: onChanged,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+
+ void onChanged(double value) {
+ setState(() {
+ _sliderValue = value;
+ _cachedAlphaSlider = _sliderValue;
+ });
+ widget.outsideSetter(color.withAlpha(_sliderValue.round()));
+ }
+}
diff --git a/feedback/lib/src/color_picker/color_picker.dart b/feedback/lib/src/color_picker/color_picker.dart
new file mode 100644
index 00000000..82d5e7bc
--- /dev/null
+++ b/feedback/lib/src/color_picker/color_picker.dart
@@ -0,0 +1,54 @@
+part of 'color_picker_icon.dart';
+
+class _ColorPicker extends StatefulWidget {
+ const _ColorPicker({
+ required this.onColorChanged,
+ required this.activeColor,
+ });
+
+ final ValueChanged onColorChanged;
+ final Color activeColor;
+
+ @override
+ State<_ColorPicker> createState() => _ColorPickerState();
+}
+
+class _ColorPickerState extends State<_ColorPicker> {
+ late Color _hueSlideColor = widget.activeColor;
+ double _whiteToColorValue = _cachedWhiteToColorValue;
+
+ void onHueColorChanged(Color color) => setState(
+ () => _hueSlideColor = color,
+ );
+
+ void onWhiteToColorChanged(double value) => setState(
+ () => _whiteToColorValue = value,
+ );
+
+ @override
+ Widget build(BuildContext context) => Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Flexible(
+ child: _WhiteToColorBox(
+ color: _hueSlideColor,
+ innerSetter: onWhiteToColorChanged,
+ outsideSetter: widget.onColorChanged,
+ ),
+ ),
+ SizedBox(height: _thumbRadius),
+ _AlphaSlider(
+ color: _hueSlideColor,
+ whiteToColorValue: _whiteToColorValue,
+ outsideSetter: widget.onColorChanged,
+ ),
+ SizedBox(height: _thumbRadius),
+ /// This is not affected by the alpha slider nor the white to color
+ _HueSlider(
+ activeColor: widget.activeColor,
+ insideSetter: onHueColorChanged,
+ outSideSetter: widget.onColorChanged,
+ ),
+ ],
+ );
+}
diff --git a/feedback/lib/src/color_picker/color_picker_icon.dart b/feedback/lib/src/color_picker/color_picker_icon.dart
new file mode 100644
index 00000000..1bdabb34
--- /dev/null
+++ b/feedback/lib/src/color_picker/color_picker_icon.dart
@@ -0,0 +1,129 @@
+import 'dart:math';
+
+import 'package:feedback/src/utilities/back_button_interceptor.dart';
+import 'package:flutter/material.dart';
+
+part 'alpha_slider.dart';
+part 'color_picker.dart';
+part 'constants.dart';
+part 'hue_slider.dart';
+part 'overlay_stack.dart';
+part 'ring_clipper.dart';
+part 'white_to_color_box.dart';
+
+///
+class ColorPickerIcon extends StatelessWidget {
+ ///
+ const ColorPickerIcon({
+ super.key,
+ required this.activeColor,
+ required this.onColorChanged,
+ });
+
+ ///
+ final ValueChanged? onColorChanged;
+
+ ///
+ final Color activeColor;
+
+ List get _brightColors => [
+ Colors.blue,
+ Colors.red,
+ Colors.yellow,
+ Colors.green,
+ ];
+
+ static const _iconSize = 30.0;
+ static const _outerRingSize = _iconSize / 7;
+ static const _innerRingSize = _outerRingSize;
+
+ // This last ` - 1 ` is for easier visual separation between alpha and hue representation
+ // especially when color is lighter while at max alpha due to [_WhiteToColorBox]
+ static const _innerColorSize = _iconSize - (_outerRingSize * 2) - (_innerRingSize * 2) - 1;
+
+ @override
+ Widget build(BuildContext context) => IconButton(
+ onPressed: onColorChanged == null ? null : () => _showColorPicker(context),
+ icon: Stack(
+ alignment: Alignment.center,
+ children: [
+ ClipPath(
+ clipper: _RingClipper(width: _outerRingSize),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ curve: Curves.decelerate,
+ width: _iconSize,
+ height: _iconSize,
+ decoration: BoxDecoration(
+ gradient: SweepGradient(
+ colors: onColorChanged == null
+ ? _brightColors.map((c) => c.withAlpha(50)).toList()
+ : _brightColors,
+ ),
+ shape: BoxShape.circle,
+ ),
+ ),
+ ),
+
+ /// That ring is the full representation of active color and its alpha
+ ClipPath(
+ clipper: _RingClipper(width: _innerRingSize),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ curve: Curves.decelerate,
+ width: _iconSize - _outerRingSize * 2,
+ height: _iconSize - _outerRingSize * 2,
+ decoration: BoxDecoration(
+ /// Found it better in indicating lower
+ /// alpha values when adding `withAlpha(170)`
+ color: Colors.white.withAlpha(170),
+ shape: BoxShape.circle,
+ ),
+ foregroundDecoration: BoxDecoration(
+ color: _borderColor,
+ shape: BoxShape.circle,
+ ),
+ ),
+ ),
+
+ /// That is the color representation without alpha
+ Container(
+ width: _innerColorSize,
+ height: _innerColorSize,
+ decoration: BoxDecoration(
+ color: _topColor,
+ shape: BoxShape.circle,
+ ),
+ ),
+ ],
+ ),
+ );
+
+ /// Solid representation of active color without alpha
+ Color get _topColor =>
+ onColorChanged == null ? activeColor.withAlpha(50) : activeColor.withAlpha(_maxAlpha);
+
+ /// Full representation of active color including alpha
+ Color get _borderColor => onColorChanged == null ? activeColor.withAlpha(50) : activeColor;
+
+ static OverlayEntry? _entry;
+
+ void _showColorPicker(BuildContext context) {
+ final overlayState = Overlay.of(context);
+
+ _entry = OverlayEntry(
+ builder: (context) => _OverlayStack(
+ closeCallback: _close,
+ activeColor: activeColor,
+ onColorChanged: onColorChanged!,
+ ),
+ );
+
+ overlayState.insert(_entry!);
+ }
+
+ bool _close() {
+ _entry?.remove();
+ return true;
+ }
+}
diff --git a/feedback/lib/src/color_picker/constants.dart b/feedback/lib/src/color_picker/constants.dart
new file mode 100644
index 00000000..366002f5
--- /dev/null
+++ b/feedback/lib/src/color_picker/constants.dart
@@ -0,0 +1,19 @@
+part of 'color_picker_icon.dart';
+
+final _colorsList = List.from(
+ Colors.primaries.take(Colors.primaries.length - 2),
+).followedBy(
+ [Colors.brown.shade500, Colors.black],
+).toList();
+final _colorShare = 1 / (_colorsList.length - 1);
+
+const _thumbRadius = 24.0;
+double _cachedHueSlider = 1.0;
+
+double _cachedAlphaSlider = _maxAlpha.toDouble();
+const _minAlpha = 50;
+const _maxAlpha = 255;
+
+final GlobalKey _whiteToColorKey = GlobalKey();
+const Color _startColor = Colors.white;
+double _cachedWhiteToColorValue = 1.0;
diff --git a/feedback/lib/src/color_picker/hue_slider.dart b/feedback/lib/src/color_picker/hue_slider.dart
new file mode 100644
index 00000000..93019f2a
--- /dev/null
+++ b/feedback/lib/src/color_picker/hue_slider.dart
@@ -0,0 +1,132 @@
+part of 'color_picker_icon.dart';
+
+class _HueSlider extends StatefulWidget {
+ const _HueSlider({
+ required this.activeColor,
+ required this.insideSetter,
+ required this.outSideSetter,
+ });
+
+ final Color activeColor;
+ final ValueSetter insideSetter;
+ final ValueSetter outSideSetter;
+
+ @override
+ State<_HueSlider> createState() => _HueSliderState();
+}
+
+class _HueSliderState extends State<_HueSlider> {
+ @override
+ void initState() {
+ super.initState();
+ Future(() {
+ _presetToActiveColorIfExists();
+ onChanged(_cachedHueSlider);
+ });
+ }
+
+ void _presetToActiveColorIfExists() {
+ /// Presetting [_cachedHueSlider] to move the slider to match the provided
+ /// [activeColor] if it is present in [_colorsList]
+ Color? matchedColor;
+ _colorsList.any(
+ (element) {
+ if (widget.activeColor == element) {
+ matchedColor = element;
+ return true;
+ }
+ return false;
+ },
+ );
+ if (matchedColor != null) {
+ final index = _colorsList.indexOf(matchedColor!);
+ _cachedHueSlider = index * _colorShare;
+ }
+ }
+
+ Color _hueSlideColor = Colors.transparent;
+ double _slideValue = _cachedHueSlider;
+
+ @override
+ Widget build(BuildContext context) => SizedBox(
+ height: _thumbRadius,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ Container(
+ height: _thumbRadius / 2,
+ margin: EdgeInsets.symmetric(horizontal: _thumbRadius),
+ decoration: ShapeDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ colors: _colorsList,
+ ),
+ shape: StadiumBorder(),
+ ),
+ ),
+ Material(
+ color: Colors.transparent,
+ child: SliderTheme(
+ data: SliderThemeData(
+ thumbShape: const RoundSliderThumbShape(
+ enabledThumbRadius: _thumbRadius * 0.65,
+ ),
+ overlayShape: const RoundSliderOverlayShape(
+ overlayRadius: _thumbRadius,
+ ),
+ ),
+ child: Slider(
+ activeColor: Colors.transparent,
+ inactiveColor: Colors.transparent,
+ thumbColor: _hueSlideColor,
+ value: _slideValue,
+ onChanged: onChanged,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+
+ void onChanged(double value) {
+ final hueColor = _getColorFromSliderValue(value);
+
+ setState(() {
+ _hueSlideColor = hueColor;
+ _slideValue = value;
+ _cachedHueSlider = _slideValue;
+ });
+ widget.insideSetter(_hueSlideColor);
+
+ /// Note that [_getColorFromSliderValue] returns a color without taking alpha
+ /// nor white to color value into account so we have to adjust it before setting
+ /// the outside [activeColor]
+ final colorWithWhiteToColor = Color.lerp(
+ _startColor,
+ _hueSlideColor,
+ _cachedWhiteToColorValue,
+ )!;
+ final allSetColor = colorWithWhiteToColor.withAlpha(_cachedAlphaSlider.round());
+
+ widget.outSideSetter(allSetColor);
+ }
+
+ Color _getColorFromSliderValue(double value) {
+ final firstIndex = value ~/ _colorShare;
+ final secondIndex = min(firstIndex + 1, _colorsList.length - 1);
+ final (first, second) = (
+ _colorsList[firstIndex],
+ _colorsList[secondIndex],
+ );
+
+ final interpolation = value / _colorShare - min(firstIndex, secondIndex);
+
+ final hueColor = Color.lerp(
+ first,
+ second,
+ interpolation,
+ );
+ return hueColor!;
+ }
+}
diff --git a/feedback/lib/src/color_picker/overlay_stack.dart b/feedback/lib/src/color_picker/overlay_stack.dart
new file mode 100644
index 00000000..763c849b
--- /dev/null
+++ b/feedback/lib/src/color_picker/overlay_stack.dart
@@ -0,0 +1,81 @@
+part of 'color_picker_icon.dart';
+
+class _OverlayStack extends StatefulWidget {
+ const _OverlayStack({
+ required this.closeCallback,
+ required this.activeColor,
+ required this.onColorChanged,
+ });
+
+ final BoolCallback closeCallback;
+ final Color activeColor;
+ final ValueChanged onColorChanged;
+
+ @override
+ State<_OverlayStack> createState() => _OverlayStackState();
+}
+
+class _OverlayStackState extends State<_OverlayStack> {
+ static const Duration _animationDuration = Duration(milliseconds: 320);
+
+ @override
+ void initState() {
+ super.initState();
+ BackButtonInterceptor.add(widget.closeCallback, priority: 1);
+ Future(() => setState(() => opacity = 1.0));
+ }
+
+ double opacity = 0.0;
+
+ void onTapOutside() {
+ setState(() => opacity = 0.0);
+ Future.delayed(_animationDuration).then(
+ (_) => widget.closeCallback(),
+ );
+ }
+
+ @override
+ void dispose() {
+ BackButtonInterceptor.remove(widget.closeCallback);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) => Stack(
+ alignment: Alignment.center,
+ children: [
+ Positioned.fill(
+ child: GestureDetector(
+ onTap: onTapOutside,
+ ),
+ ),
+ AnimatedOpacity(
+ duration: _animationDuration,
+ opacity: opacity,
+ curve: Curves.decelerate,
+ child: Container(
+ constraints: BoxConstraints(
+ maxWidth: min(
+ 450,
+ MediaQuery.sizeOf(context).width,
+ ),
+ maxHeight: min(
+ 450,
+ MediaQuery.sizeOf(context).height * 0.85,
+ ),
+ ),
+ margin: EdgeInsets.symmetric(horizontal: 20, vertical: 40),
+ padding: EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceDim,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: _ColorPicker(
+ activeColor: widget.activeColor,
+ onColorChanged: widget.onColorChanged,
+ ),
+ ),
+ ),
+ ],
+ );
+}
diff --git a/feedback/lib/src/color_picker/ring_clipper.dart b/feedback/lib/src/color_picker/ring_clipper.dart
new file mode 100644
index 00000000..6641b770
--- /dev/null
+++ b/feedback/lib/src/color_picker/ring_clipper.dart
@@ -0,0 +1,31 @@
+part of 'color_picker_icon.dart';
+
+class _RingClipper extends CustomClipper {
+ final double width;
+
+ const _RingClipper({required this.width});
+
+ @override
+ Path getClip(Size size) {
+ final outerRadius = size.width / 2;
+ assert(size.width == size.height, 'Width is not equal to height');
+
+ final path = Path()..fillType = PathFillType.evenOdd;
+ path.addOval(
+ Rect.fromCircle(
+ center: Offset(outerRadius, outerRadius),
+ radius: outerRadius,
+ ),
+ );
+ path.addOval(
+ Rect.fromCircle(
+ center: Offset(outerRadius, outerRadius),
+ radius: outerRadius - width,
+ ),
+ );
+ return path;
+ }
+
+ @override
+ bool shouldReclip(covariant CustomClipper oldClipper) => true;
+}
diff --git a/feedback/lib/src/color_picker/white_to_color_box.dart b/feedback/lib/src/color_picker/white_to_color_box.dart
new file mode 100644
index 00000000..9a937cee
--- /dev/null
+++ b/feedback/lib/src/color_picker/white_to_color_box.dart
@@ -0,0 +1,285 @@
+part of 'color_picker_icon.dart';
+
+class _WhiteToColorBox extends StatefulWidget {
+ const _WhiteToColorBox({
+ required this.color,
+ required this.outsideSetter,
+ required this.innerSetter,
+ });
+
+ final Color color;
+ final ValueSetter outsideSetter;
+ final ValueSetter innerSetter;
+
+ @override
+ State<_WhiteToColorBox> createState() => _WhiteToColorBoxState();
+}
+
+class _WhiteToColorBoxState extends State<_WhiteToColorBox> {
+ static const double _padding = _thumbRadius * 0.65;
+ static const AlignmentGeometry _startAlignment = Alignment.topLeft;
+ static const AlignmentGeometry _endAlignment = Alignment.bottomRight;
+
+ Color get color => widget.color.withAlpha(_maxAlpha);
+
+ @override
+ void initState() {
+ super.initState();
+ Future(
+ () => setState(
+ () => offset = _calculateInitialOffset(),
+ ),
+ );
+ }
+
+ @override
+ didUpdateWidget(covariant _WhiteToColorBox oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ setState(
+ () => interpolatedColor = Color.lerp(
+ _startColor,
+ color,
+ _cachedWhiteToColorValue,
+ )!,
+ );
+ }
+
+ late Offset offset = Offset.zero;
+ late Color interpolatedColor = Color.lerp(
+ _startColor,
+ color,
+ _cachedWhiteToColorValue,
+ )!;
+
+ @override
+ Widget build(BuildContext context) => Stack(
+ key: _whiteToColorKey,
+ children: [
+ AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ margin: EdgeInsets.all(_padding),
+ // decoration: BoxDecoration(
+ // image: DecorationImage(
+ // image: AssetImage('assets/checkerboard.png'),
+ // fit: BoxFit.fill,
+ // ),
+ // ),
+ foregroundDecoration: ShapeDecoration(
+ /// If This [LinearGradient] changes, It will need another algorithm to calculate
+ /// the interpolated color according to the new gradient type.
+ gradient: LinearGradient(
+ /// These may change freely even with [AlignmentDirectional] values
+ begin: _startAlignment,
+ end: _endAlignment,
+ colors: [
+ _startColor,
+ color,
+ ],
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ side: BorderSide(width: 1),
+ ),
+ ),
+ ),
+ Positioned.fill(
+ child: GestureDetector(
+ onTapDown: (details) => _onPointerUpdate(details.globalPosition),
+ onPanUpdate: (details) => _onPointerUpdate(details.globalPosition),
+ ),
+ ),
+ Positioned(
+ top: offset.dy,
+ left: offset.dx,
+ child: GestureDetector(
+ onPanUpdate: (details) => _onPointerUpdate(details.globalPosition),
+ child: Container(
+ width: _padding * 2,
+ height: _padding * 2,
+ decoration: ShapeDecoration(
+ shape: CircleBorder(
+ side: BorderSide(width: 1),
+ ),
+ color: interpolatedColor,
+ ),
+ ),
+ ),
+ )
+ ],
+ );
+
+ Offset _calculateInitialOffset() {
+ final renderBox = _whiteToColorKey.currentContext!.findRenderObject() as RenderBox;
+
+ /// Note we have a margin of [padding] all around the box.
+ final xUpperLimit = renderBox.size.width - _padding * 2;
+ final yUpperLimit = renderBox.size.height - _padding * 2;
+
+ final bottomRight = Offset(xUpperLimit, yUpperLimit);
+
+ final (start, end) = _startEnd(
+ bottomRight: bottomRight,
+ direction: Directionality.of(context),
+ );
+
+ /// m = (h - 0) / ( w - 0 )
+ /// c = y - mx
+ final m = (end.dy - start.dy) / (end.dx - start.dx);
+ final c = start.dy - m * start.dx;
+
+ final gradientLineLength = calculateDistance(start, end);
+ final distanceOfInterpolation = gradientLineLength * _cachedWhiteToColorValue;
+
+ final otherPoint = calculateOtherPoint(
+ start,
+ distanceOfInterpolation,
+ m,
+ c,
+ );
+
+ return otherPoint;
+ }
+
+ double calculateDistance(Offset start, Offset end) => sqrt(
+ pow(end.dx - start.dx, 2) + pow(end.dy - start.dy, 2),
+ );
+
+ Offset calculateOtherPoint(
+ Offset point,
+ double distance,
+ double lineSlope,
+ double coefficient,
+ ) {
+ /// There values are the coefficients of the quadratic equation
+ /// obtained from solving the given line equation and the distance equation.
+ /// y1 = m * x1 + c;
+ /// d = sqrt( (x1 - x0)^2 + (y1 - y0)^2 )
+ final a = 1 + lineSlope * lineSlope;
+ final b = 2 * lineSlope * coefficient - 2 * point.dx - point.dy * lineSlope;
+ final c = pow(point.dx, 2) -
+ point.dy * coefficient +
+ pow(point.dy, 2) +
+ pow(coefficient, 2) -
+ pow(distance, 2);
+
+ double x;
+ x = (-b + sqrt(b * b - 4 * a * c)) / (2 * a);
+ if (x.isNegative) x = (-b - sqrt(b * b - 4 * a * c)) / (2 * a);
+
+ final y = lineSlope * x + coefficient;
+ return Offset(x, y);
+ }
+
+ void _onPointerUpdate(Offset global) {
+ final renderBox = _whiteToColorKey.currentContext!.findRenderObject() as RenderBox;
+
+ /// Note we have a margin of [padding] all around the box.
+ final boxTopLeftGlobalOffset = renderBox.localToGlobal(Offset(_padding, _padding));
+ final xUpperLimit = renderBox.size.width - _padding * 2;
+ final yUpperLimit = renderBox.size.height - _padding * 2;
+
+ final local = global - boxTopLeftGlobalOffset;
+ final tapPosition = Offset(
+ local.dx.clamp(0, xUpperLimit),
+ local.dy.clamp(0, yUpperLimit),
+ );
+
+ final bottomRight = Offset(xUpperLimit, yUpperLimit);
+ final newColor = _interpolateTapPosition(
+ bottomRight,
+ tapPosition,
+ );
+ setState(
+ () {
+ offset = tapPosition;
+ interpolatedColor = newColor;
+ },
+ );
+ widget.innerSetter(_cachedWhiteToColorValue);
+ final colorWithAlpha = interpolatedColor.withAlpha(_cachedAlphaSlider.round());
+ widget.outsideSetter(colorWithAlpha);
+ }
+
+ Color _interpolateTapPosition(Offset bottomRight, Offset tapPosition) {
+ final x0 = tapPosition.dx;
+ final y0 = tapPosition.dy;
+
+ final (start, end) = _startEnd(
+ bottomRight: bottomRight,
+ direction: Directionality.of(context),
+ );
+
+ /// m = (h - 0) / ( w - 0 )
+ /// c = y - mx
+ final m = (end.dy - start.dy) / (end.dx - start.dx);
+ final c = start.dy - m * start.dx;
+
+ /// x1 = ( x0 + m y0 - m c ) / ( m^2 + 1 )
+ /// y1 = m (x1) + c
+ final x1 = (x0 + m * y0 - m * c) / (m * m + 1);
+ final y1 = m * x1 + c;
+
+ /// Performing Interpolation
+ final distance = calculateDistance(start, Offset(x1, y1));
+ final gradientLineLength = calculateDistance(start, end);
+ final interpolationValue = distance / gradientLineLength;
+
+ /// Caching interpolation value
+ _cachedWhiteToColorValue = interpolationValue;
+
+ return Color.lerp(
+ _startColor,
+ color,
+ interpolationValue,
+ )!;
+ }
+
+ (Offset start, Offset end) _startEnd({
+ required Offset bottomRight,
+ required TextDirection direction,
+ }) {
+ final startAlignment = _startAlignment.resolve(direction);
+ final endAlignment = _endAlignment.resolve(direction);
+
+ final double startXOffset;
+ final double startYOffset;
+ if (startAlignment.x < 0) {
+ startXOffset = 0;
+ } else if (startAlignment.x == 0) {
+ startXOffset = bottomRight.dx / 2;
+ } else {
+ startXOffset = bottomRight.dx;
+ }
+
+ if (startAlignment.y < 0) {
+ startYOffset = 0;
+ } else if (startAlignment.y == 0) {
+ startYOffset = bottomRight.dy / 2;
+ } else {
+ startYOffset = bottomRight.dy;
+ }
+
+ final double endXOffset;
+ final double endYOffset;
+ if (endAlignment.x < 0) {
+ endXOffset = 0;
+ } else if (endAlignment.x == 0) {
+ endXOffset = bottomRight.dx / 2;
+ } else {
+ endXOffset = bottomRight.dx;
+ }
+
+ if (endAlignment.y < 0) {
+ endYOffset = 0;
+ } else if (endAlignment.y == 0) {
+ endYOffset = bottomRight.dy / 2;
+ } else {
+ endYOffset = bottomRight.dy;
+ }
+
+ return (
+ Offset(startXOffset, startYOffset),
+ Offset(endXOffset, endYOffset),
+ );
+ }
+}
diff --git a/feedback/lib/src/controls_column.dart b/feedback/lib/src/controls_column.dart
index 27eec0d1..3cf22b21 100644
--- a/feedback/lib/src/controls_column.dart
+++ b/feedback/lib/src/controls_column.dart
@@ -5,6 +5,9 @@ import 'package:feedback/src/l18n/translation.dart';
import 'package:feedback/src/theme/feedback_theme.dart';
import 'package:flutter/material.dart';
+import 'color_picker/color_picker_icon.dart';
+import 'utilities/custom_color_position.dart';
+
/// This is the Widget on the right side of the app when the feedback view
/// is active.
class ControlsColumn extends StatelessWidget {
@@ -19,11 +22,14 @@ class ControlsColumn extends StatelessWidget {
required this.onCloseFeedback,
required this.onClearDrawing,
required this.colors,
- }) : assert(
+ required this.showCustomColor,
+ required this.customColorPosition,
+ }) : assert(
colors.isNotEmpty,
'There must be at least one color to draw in colors',
- ),
- assert(colors.contains(activeColor), 'colors must contain activeColor');
+ );
+
+ // assert(colors.contains(activeColor), 'colors must contain activeColor');
final ValueChanged onColorChanged;
final VoidCallback onUndo;
@@ -31,6 +37,8 @@ class ControlsColumn extends StatelessWidget {
final VoidCallback onCloseFeedback;
final VoidCallback onClearDrawing;
final List colors;
+ final bool showCustomColor;
+ final CustomColorPosition customColorPosition;
final Color activeColor;
final FeedbackMode mode;
@@ -91,6 +99,11 @@ class ControlsColumn extends StatelessWidget {
icon: const Icon(Icons.delete),
onPressed: isNavigatingActive ? null : onClearDrawing,
),
+ if (customColorPosition.isLeading && showCustomColor)
+ ColorPickerIcon(
+ onColorChanged: isNavigatingActive ? null : onColorChanged,
+ activeColor: activeColor,
+ ),
for (final color in colors)
_ColorSelectionIconButton(
key: ValueKey(color),
@@ -98,6 +111,11 @@ class ControlsColumn extends StatelessWidget {
onPressed: isNavigatingActive ? null : onColorChanged,
isActive: activeColor == color,
),
+ if (customColorPosition.isTrailing && showCustomColor)
+ ColorPickerIcon(
+ onColorChanged: isNavigatingActive ? null : onColorChanged,
+ activeColor: activeColor,
+ ),
],
),
);
diff --git a/feedback/lib/src/feedback_widget.dart b/feedback/lib/src/feedback_widget.dart
index f22acedb..36c6e3e7 100644
--- a/feedback/lib/src/feedback_widget.dart
+++ b/feedback/lib/src/feedback_widget.dart
@@ -220,6 +220,8 @@ class FeedbackWidgetState extends State
mode: mode,
activeColor: painterController.drawColor,
colors: widget.drawColors,
+ showCustomColor: feedbackThemeData.showCustomColor,
+ customColorPosition: feedbackThemeData.customColorPosition,
onColorChanged: (color) {
setState(() {
painterController.drawColor = color;
diff --git a/feedback/lib/src/theme/feedback_theme.dart b/feedback/lib/src/theme/feedback_theme.dart
index 5befc0fb..d558e0db 100644
--- a/feedback/lib/src/theme/feedback_theme.dart
+++ b/feedback/lib/src/theme/feedback_theme.dart
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
+import '../utilities/custom_color_position.dart';
+
const _defaultDrawColors = [
Colors.red,
Colors.green,
@@ -38,6 +40,8 @@ class FeedbackThemeData {
this.feedbackSheetHeight = .25,
this.activeFeedbackModeColor = _blue,
this.drawColors = _defaultDrawColors,
+ this.showCustomColor = true,
+ this.customColorPosition = CustomColorPosition.trailing,
this.bottomSheetDescriptionStyle = _defaultBottomSheetDescriptionStyle,
this.bottomSheetTextInputStyle = _defaultBottomSheetTextInputStyle,
this.sheetIsDraggable = true,
@@ -108,6 +112,12 @@ class FeedbackThemeData {
/// Colors which can be used to draw while in feedback mode.
final List drawColors;
+ /// Determines whether or not to show the custom color picker icon.
+ final bool showCustomColor;
+
+ /// Determines the position of the custom color picker icon.
+ final CustomColorPosition customColorPosition;
+
/// Text Style of the text above of the feedback text input.
final TextStyle bottomSheetDescriptionStyle;
diff --git a/feedback/lib/src/utilities/back_button_interceptor.dart b/feedback/lib/src/utilities/back_button_interceptor.dart
index a661673d..c5bbdd0c 100644
--- a/feedback/lib/src/utilities/back_button_interceptor.dart
+++ b/feedback/lib/src/utilities/back_button_interceptor.dart
@@ -15,17 +15,18 @@ class BackButtonInterceptor with WidgetsBindingObserver {
static final SplayTreeMap> _prioritizedCallbacks =
SplayTreeMap();
+ /// priority of 0 is considered the highest priority.
static void add(BoolCallback callback, {int? priority}) {
+ assert(priority == null || priority >= 0, 'Priority ,if not omitted, must be >= 0');
if (_prioritizedCallbacks.isEmpty) {
_mount();
}
- final List? callbacksList = _prioritizedCallbacks[priority];
+ // Convert to double so that we have a valid maximum value to sort null
+ // priorities last.
+ final doublePriority = priority?.toDouble() ?? double.infinity;
+ final List? callbacksList = _prioritizedCallbacks[doublePriority];
if (callbacksList == null) {
- // Convert to double so that we have a valid maximum value to sort null
- // priorities last.
- _prioritizedCallbacks[priority?.toDouble() ?? double.infinity] = [
- callback
- ];
+ _prioritizedCallbacks[doublePriority] = [callback];
return;
}
callbacksList.add(callback);
diff --git a/feedback/lib/src/utilities/custom_color_position.dart b/feedback/lib/src/utilities/custom_color_position.dart
new file mode 100644
index 00000000..269b59a4
--- /dev/null
+++ b/feedback/lib/src/utilities/custom_color_position.dart
@@ -0,0 +1,14 @@
+/// Position of the custom color picker icon.
+enum CustomColorPosition {
+ /// Places the custom color picker icon at the BEGINNING of the drawing colors list.
+ leading,
+
+ /// Places the custom color picker icon at the END of the drawing colors list.
+ trailing;
+
+ /// Returns true if the icon is placed at the beginning of the drawing colors list.
+ bool get isLeading => this == CustomColorPosition.leading;
+
+ /// Returns true if the icon is placed at the end of the drawing colors list.
+ bool get isTrailing => this == CustomColorPosition.trailing;
+}
diff --git a/feedback/test/controls_column_test.dart b/feedback/test/controls_column_test.dart
index 0560ff1b..32328b9e 100644
--- a/feedback/test/controls_column_test.dart
+++ b/feedback/test/controls_column_test.dart
@@ -1,8 +1,9 @@
import 'package:feedback/src/controls_column.dart';
import 'package:feedback/src/feedback_mode.dart';
import 'package:feedback/src/l18n/localization.dart';
-import 'package:flutter_test/flutter_test.dart';
+import 'package:feedback/src/utilities/custom_color_position.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
void main() {
Widget create({
@@ -14,6 +15,8 @@ void main() {
VoidCallback? onCloseFeedback,
VoidCallback? onClearDrawing,
List? colors,
+ bool? showCustomColor,
+ CustomColorPosition? customColorPosition,
}) {
return FeedbackLocalization(
child: ControlsColumn(
@@ -26,6 +29,8 @@ void main() {
onColorChanged: onColorChanged ?? (newColor) {},
onControlModeChanged: onControlModeChanged ?? (newMode) {},
onUndo: onUndo ?? () {},
+ showCustomColor: showCustomColor ?? true,
+ customColorPosition: customColorPosition ?? CustomColorPosition.trailing,
),
);
}