Last active
October 24, 2023 11:04
-
-
Save pagetronic/e0cf25fe74bc99c425b8459a84e0199e to your computer and use it in GitHub Desktop.
Simple color picker for Flutter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math'; | |
import 'dart:typed_data'; | |
import 'dart:ui' as ui; | |
import 'package:flutter/material.dart'; | |
class ColorPicker extends StatefulWidget { | |
final ValueNotifier<Color?> color; | |
const ColorPicker({super.key, required this.color}); | |
@override | |
ColorPickerState createState() => ColorPickerState(); | |
} | |
class ColorPickerState extends State<ColorPicker> { | |
@override | |
Widget build(BuildContext context) { | |
SlidePainter slider = SlidePainter.get(widget.color); | |
return LayoutBuilder( | |
builder: (context, constraints) { | |
slider.setWidth(constraints.maxWidth); | |
return SizedBox( | |
width: constraints.maxWidth, | |
height: 30, | |
child: GestureDetector( | |
onHorizontalDragUpdate: (DragUpdateDetails details) { | |
slider.setColor(details.localPosition.dx.toInt()); | |
}, | |
onTapDown: (TapDownDetails details) { | |
slider.setColor(details.localPosition.dx.toInt()); | |
}, | |
child: CustomPaint(size: Size(constraints.maxWidth, constraints.maxHeight), painter: slider), | |
), | |
); | |
}, | |
); | |
} | |
} | |
class SlidePainter extends CustomPainter { | |
final List<Color> gradient = const [ | |
Color.fromARGB(255, 255, 0, 0), | |
Color.fromARGB(255, 255, 128, 0), | |
Color.fromARGB(255, 255, 255, 0), | |
Color.fromARGB(255, 128, 255, 0), | |
Color.fromARGB(255, 0, 255, 0), | |
Color.fromARGB(255, 0, 255, 128), | |
Color.fromARGB(255, 0, 255, 255), | |
Color.fromARGB(255, 0, 128, 255), | |
Color.fromARGB(255, 0, 0, 255), | |
Color.fromARGB(255, 127, 0, 255), | |
Color.fromARGB(255, 255, 0, 255), | |
Color.fromARGB(255, 255, 0, 127), | |
]; | |
double width = 1; | |
final ValueNotifier<Color?> color; | |
final Notifier repaint; | |
double position = -1; | |
final double padding = 14; | |
Future<void> future = Future(() {}); | |
Future<ByteData?>? byteDataFuture; | |
SlidePainter(this.color, this.repaint) : super(repaint: repaint); | |
static SlidePainter get(final ValueNotifier<Color?> color) { | |
return SlidePainter(color, Notifier()); | |
} | |
@override | |
void paint(Canvas canvas, Size size) { | |
Rect rect = Offset.zero & size; | |
canvas.clipRect(rect); | |
canvas.drawRRect( | |
RRect.fromRectAndRadius(Rect.fromPoints(Offset(0, 5 * size.height / 18), Offset(size.width, 13 * size.height / 18)), const Radius.circular(4)), | |
Paint() | |
..shader = ui.Gradient.linear( | |
Offset(padding - 2, 0), | |
Offset(size.width - padding - 2, 0), | |
gradient, | |
[for (int index = 0; index < gradient.length; index++) index / gradient.length], | |
), | |
); | |
canvas.drawRRect( | |
RRect.fromRectAndRadius(Rect.fromPoints(Offset(0, 5 * size.height / 18), Offset(size.width, 13 * size.height / 18)), const Radius.circular(4)), | |
Paint() | |
..color = Colors.grey.withOpacity(0.7) | |
..strokeWidth = 1 | |
..style = PaintingStyle.stroke, | |
); | |
canvas.drawCircle( | |
Offset(position, size.height / 2), | |
12, | |
Paint() | |
..color = Colors.white.withOpacity(0.7) | |
..strokeWidth = 4 | |
..style = PaintingStyle.stroke, | |
); | |
canvas.drawCircle( | |
Offset(position, size.height / 2), | |
12, | |
Paint() | |
..color = color.value != null ? color.value! : Colors.grey | |
..shader = ui.Gradient.radial( | |
Offset(position, size.height / 2), | |
12, | |
[color.value != null ? color.value! : Colors.grey, Colors.grey], | |
[0.75, 1], | |
), | |
); | |
} | |
@override | |
bool shouldRepaint(SlidePainter oldDelegate) { | |
return true; | |
} | |
void setColor(int dx) async { | |
dx = min(width.toInt() - padding.toInt() - 1, max(padding.toInt(), dx)); | |
await future; | |
future = Future(() async { | |
byteDataFuture ??= buildByteData(); | |
ByteData? byteData = await byteDataFuture; | |
if (byteData != null) { | |
if (dx < padding) { | |
position = padding; | |
} else if (dx >= width - padding - 1) { | |
position = width - padding - 1; | |
} else { | |
int position_ = dx; | |
position = position_.toDouble(); | |
} | |
color.value = pixelColorAt(position.toInt(), byteData); | |
repaint.notify(); | |
} | |
}); | |
} | |
void setPosition() async { | |
Color? color = this.color.value; | |
if (color == null) { | |
position = width / 2 - padding; | |
repaint.notify(); | |
return; | |
} | |
byteDataFuture ??= buildByteData(); | |
ByteData? byteData = await byteDataFuture; | |
if (byteData == null) { | |
return; | |
} | |
for (double position = padding; position < width - padding - 1; position++) { | |
if (pixelColorAt(position.toInt(), byteData) == color) { | |
this.position = position.toDouble(); | |
repaint.notify(); | |
return; | |
} | |
} | |
List<double> diffs = []; | |
for (double position = padding; position < width - padding - 1; position++) { | |
Color color_ = pixelColorAt(position.toInt(), byteData); | |
int diffRed = (color.red - color_.red).abs(); | |
int diffGreen = (color.green - color_.green).abs(); | |
int diffBlue = (color.blue - color_.blue).abs(); | |
diffs.add((diffRed + diffGreen + diffBlue) / 3); | |
} | |
double mini = double.infinity; | |
for (double diff in diffs) { | |
mini = min(diff, mini); | |
} | |
if (mini != double.infinity) { | |
position = diffs.indexOf(mini).toDouble(); | |
repaint.notify(); | |
} | |
} | |
void setWidth(double width) { | |
if (this.width != width) { | |
double oldWidth = this.width; | |
this.width = width; | |
byteDataFuture = null; | |
if (position >= 0) { | |
position = position * width / oldWidth; | |
repaint.notify(); | |
} else { | |
setPosition(); | |
} | |
} | |
} | |
Future<ByteData?> buildByteData() async { | |
ui.PictureRecorder recorder = ui.PictureRecorder(); | |
Canvas canvas = Canvas(recorder, Rect.fromPoints(Offset.zero, Offset(width, 1))); | |
canvas.drawRect(Rect.fromPoints(Offset(padding, 0), Offset(width - padding, 1)), Paint()..color = Colors.black); | |
canvas.drawRect( | |
Rect.fromPoints(Offset(padding, 0), Offset(width - padding, 1)), | |
Paint() | |
..shader = ui.Gradient.linear( | |
Offset(padding, 0), | |
Offset(width - padding, 1), | |
gradient, | |
[for (int index = 0; index < gradient.length; index++) index / gradient.length], | |
), | |
); | |
return await (await recorder.endRecording().toImage(width.toInt(), 1)).toByteData(format: ui.ImageByteFormat.rawRgba); | |
} | |
Color pixelColorAt(int x, ByteData? byteData) { | |
if (byteData == null || x < 0 || x >= width) { | |
return Colors.transparent; | |
} | |
int byteOffset = 4 * (x); | |
int rgbaColor = byteData.getUint32(byteOffset); | |
int a = rgbaColor & 0xFF; | |
int rgb = rgbaColor >> 8; | |
return Color(rgb + (a << 24)); | |
} | |
} | |
class Notifier extends ChangeNotifier { | |
void notify() { | |
notifyListeners(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment