之前做了一个Android的颜色选择器,不过没开源,闲暇时间做了个flutter颜色选择器,废话不多说先看效果:
这个可以显示任意位置,大小,但是大小不能超过屏幕。
直接上代码吧:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'dart:math';
typedef SelectColor = Color Function(Color color);
class ColorPickView extends StatefulWidget {
Size size;
double selectRadius;
double padding;
Color selectColor;
Color selectRingColor;
final SelectColor selectColorCallBack;
ColorPickView(
{this.size,
this.selectColorCallBack,
this.selectRadius,
this.padding,
this.selectRingColor,
this.selectColor}) {
assert(size == null || (size != null && size.height == size.width),
'控件宽高必须相等');
}
@override
State createState() {
return ColorPickState();
}
}
class ColorPickState extends State {
double radius;
Color currentColor = Color(0xff00ff);
Offset currentOffset;
Offset topLeftPosition;
Offset selectPosition;
Size screenSize;
GlobalKey globalKey = new GlobalKey();
bool isTap = false;
@override
Widget build(BuildContext context) {
screenSize ??= MediaQuery.of(context).size;
widget.size ??= screenSize;
widget.selectRadius ??= 10;
widget.padding ??= 40;
widget.selectRingColor ??= Colors.black;
assert(
widget.size == null ||
(widget.size != null && screenSize.width >= widget.size.width),
'控件宽度太宽');
radius = widget.size.width / 2 - widget.padding;
currentOffset ??= Offset(radius, radius);
if (widget.selectColor != null && selectPosition == null)
_setColor(widget.selectColor);
_initLeftTop();
return GestureDetector(
key: globalKey,
child: Container(
width: widget.size.width,
height: widget.size.width,
child: Stack(
alignment: Alignment.center,
children: [
CustomPaint(
painter: ColorPick(radius: radius),
size: widget.size,
),
Positioned(
left: isTap
? currentOffset.dx -
(topLeftPosition == null ? 0 : (topLeftPosition.dx+widget.selectRadius/2))
: (selectPosition == null ? radius : selectPosition.dx+widget.selectRadius/2),
top: isTap
? currentOffset.dy -
(topLeftPosition == null ? 0 : (topLeftPosition.dy+widget.selectRadius/2))
: (selectPosition == null ? radius : selectPosition.dy+widget.selectRadius/2),
//这里减去80,是因为上下边距各40 所以需要减去还有半径
child: Container(
width: widget.selectRadius,
height: widget.selectRadius,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.selectRadius),
border: Border.fromBorderSide(
BorderSide(color: widget.selectRingColor)),
),
child: ClipOval(
child: Container(
color: currentColor,
),
),
),
),
],
),
),
onTapDown: (e) {
setState(() {
isTap = true;
_initLeftTop();
if (!isOutSide(e.globalPosition.dx, e.globalPosition.dy)) {
currentColor =
getColorAtPoint(e.globalPosition.dx, e.globalPosition.dy);
currentOffset = e.globalPosition;
if (widget.selectColorCallBack != null) {
widget.selectColorCallBack(currentColor);
}
}
});
},
onPanUpdate: (e) {
isTap = true;
_initLeftTop();
setState(() {
if (!isOutSide(e.globalPosition.dx, e.globalPosition.dy)) {
currentOffset = e.globalPosition;
currentColor =
getColorAtPoint(e.globalPosition.dx, e.globalPosition.dy);
if (widget.selectColorCallBack != null) {
widget.selectColorCallBack(currentColor);
}
}
});
},
);
}
void _initLeftTop() {
if (globalKey.currentContext != null && topLeftPosition == null) {
final RenderBox box = globalKey.currentContext.findRenderObject();
topLeftPosition = box.localToGlobal(Offset.zero);
}
}
bool isOutSide(double eventX, double eventY) {
double x = eventX - (topLeftPosition.dx + radius + widget.padding);
double y = eventY - (topLeftPosition.dy + radius + widget.padding);
double r = sqrt(x * x + y * y);
if (r >= radius) return true;
return false;
}
void _setColor(Color color) {
//设置颜色值
var hsvColor = HSVColor.fromColor(color);
double r = hsvColor.saturation * radius;
double radian = hsvColor.hue / -180.0 * pi;
_updateSelector(r * cos(radian), -r * sin(radian));
currentColor = color;
}
void _updateSelector(double eventX, double eventY) {
//更新选中颜色值
double r = sqrt(eventX * eventX + eventY * eventY);
double x = eventX, y = eventY;
if (r > radius) {
x *= radius / r;
y *= radius / r;
}
selectPosition =
new Offset(x + radius + widget.padding, y + radius + widget.padding);
}
Color getColorAtPoint(double eventX, double eventY) {
//获取坐标在色盘中的颜色值
double x = eventX - (topLeftPosition.dx + radius + widget.padding);
double y = eventY - (topLeftPosition.dy + radius + widget.padding);
double r = sqrt(x * x + y * y);
List hsv = [0.0, 0.0, 1.0];
hsv[0] = (atan2(-y, -x) / pi * 180).toDouble() + 180;
hsv[1] = max(0, min(1, (r / radius)));
return HSVColor.fromAHSV(1.0, hsv[0], hsv[1], hsv[2]).toColor();
}
}
class ColorPick extends CustomPainter {
Paint mPaint;
Paint saturationPaint;
final List mCircleColors = new List();
final List mStatColors = new List();
SweepGradient hueShader;
final radius;
RadialGradient saturationShader;
ColorPick({this.radius}) {
_init();
}
void _init() {
//{Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA, Color.RED}
mPaint = new Paint();
saturationPaint = new Paint();
mCircleColors.add(Color.fromARGB(255, 255, 0, 0));
mCircleColors.add(Color.fromARGB(255, 255, 255, 0));
mCircleColors.add(Color.fromARGB(255, 0, 255, 0));
mCircleColors.add(Color.fromARGB(255, 0, 255, 255));
mCircleColors.add(Color.fromARGB(255, 0, 0, 255));
mCircleColors.add(Color.fromARGB(255, 255, 0, 255));
mCircleColors.add(Color.fromARGB(255, 255, 0, 0));
mStatColors.add(Color.fromARGB(255, 255, 255, 255));
mStatColors.add(Color.fromARGB(0, 255, 255, 255));
hueShader = new SweepGradient(colors: mCircleColors);
saturationShader = new RadialGradient(colors: mStatColors);
}
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);
mPaint.shader = hueShader.createShader(rect);
saturationPaint.shader = saturationShader.createShader(rect);
// 注意这一句
canvas.clipRect(rect);
canvas.drawCircle(Offset(size.width / 2, size.height / 2), radius, mPaint);
canvas.drawCircle(
Offset(size.width / 2, size.height / 2), radius, saturationPaint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
主页面类:
import 'package:flutter/material.dart';
import 'color_pick.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
Color currentColor = Color(0xff0000ff);
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Column(
children: [
ColorPickView(
selectColor: Color(0xff0000ff),
selectColorCallBack: (color) {
print(color);
setState(() {
currentColor = color;
});
},
),
Container(
color: currentColor,
height: 100,
width: 100,
child: SizedBox(),
)
],
),
);
}
}
其实这里面主要的是绘制和颜色渐变问题,自行绘制这里直接继承CustomPainter,这个继承类里面有两个paint,一个是绘制颜色域的 mPaint,另一个是绘制亮度的saturationPaint,主要用的是SweepGradient扫描/梯度渲染,需要确认圆心坐标。其实就是我们控件宽高的一半。RadialGradient环形渲染。
其实这里绘制比Android简单许多,主要是颜色值获取和选中区域跟随手指移动的问题。
颜色获取:
我们颜色可以分为H、S、V 可以这么解释直接百度如下:
用角度度量,取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。它们的补色是:黄色为60°,青色为180°,品红为300°;
饱和度S表示颜色接近光谱色的程度。一种颜色,可以看成是某种光谱色与白色混合的结果。其中光谱色所占的比例愈大,颜色接近光谱色的程度就愈高,颜色的饱和度也就愈高。饱和度高,颜色则深而艳。光谱色的白光成分为0,饱和度达到最高。通常取值范围为0~1,值越大,颜色越饱和。
明度表示颜色明亮的程度,对于光源色,明度值与发光体的光亮度有关;对于物体色,此值和物体的透射比或反射比有关。通常取值范围为0~1。
因此我们可以根据点到圆心距离算出角度代表H 点到圆心的距离与半径比为 S,亮度默认最大不用管。就会有如下算法:
Color getColorAtPoint(double eventX, double eventY) {
//获取坐标在色盘中的颜色值
double x = eventX - (topLeftPosition.dx + radius + widget.padding);
double y = eventY - (topLeftPosition.dy + radius + widget.padding);
double r = sqrt(x * x + y * y);
List hsv = [0.0, 0.0, 1.0];
hsv[0] = (atan2(-y, -x) / pi * 180).toDouble() + 180;
hsv[1] = max(0, min(1, (r / radius)));
return HSVColor.fromAHSV(1.0, hsv[0], hsv[1], hsv[2]).toColor();
}
这个其实就是高中学的数学知识,只不过基本都忘了。这里还支持反选,颜色值确定位置,其实这也就是上面的方法逆向过程,如下:
void _setColor(Color color) {
_initLeftTop();
//设置颜色值
var hsvColor = HSVColor.fromColor(color);
double r = hsvColor.saturation * radius;
double radian = hsvColor.hue / -180.0 * pi;
_updateSelector(r * cos(radian), -r * sin(radian));
currentColor = color;
}
void _updateSelector(double eventX, double eventY) {
//更新选中颜色值
double r = sqrt(eventX * eventX + eventY * eventY);
double x = eventX, y = eventY;
if (r > radius) {
x *= radius / r;
y *= radius / r;
}
currentOffset = new Offset(x + topLeftPosition.dx + radius + widget.padding,
y + topLeftPosition.dy + radius + widget.padding);
}
但是这里就需要确定一个问题,就是控件x,y做标,这里用到了这个方法获取x、y做标:
final RenderBox box = globalKey.currentContext.findRenderObject();
topLeftPosition = box.localToGlobal(Offset.zero);
不过这个方法有个缺点就是必须渲染完成才能用。
因为选中圈是根据当前控件布局的,而这个触发事件随手指移动的坐标是根据整个屏幕的,因此这个控件的x、y做标需要获取当前控件的x、y坐标,再用触发事件坐标减去当前坐标才能得到选中圈真正的坐标。
github
pub地址