Flutter自定义控件初探
前言
最近这段时间,在看Flutter绘制的相关知识。主要是看张风捷特烈的绘制小册。通过对小册的学习了解Flutter绘制的相关知识,见识到了绚丽多姿的Flutter绘制世界。 此文是对绘制小册内容的一个实践,通过一个自定义控件的案例,初步探索如何进行Flutter自定义控件的开发。
话不多说,让我们开始今天的绘制之旅吧。
一,初始版本自定义控件效果图:
我把上述的控件命名为同心边框图形,顾名思义,该控件是由多个半径不同的圆形边框组成的自定义控件,且这些边框拥有相同的中心点。
二,控件需求拆解:
经过拆解分析,该控件的功能可以分为以下几点:
1,不同圆形的半径是等差递增的,即两两相邻的圆形间隔是相等的。
2,支持业务侧传入参数,来确定图形的颜色。
3,底部的两个Slider用来控制自定义控件的中心点。
4,支持每秒切换圆形边框的颜色。
2.1 同心圆的绘制
我们先完成中间多个同心圆的绘制, 很明显对于自定义控件我们要自定义CustomPainter,从而拿到Canvas进行绘制。对于同心圆来说,需要三个参数,即最小圆半径,最大圆半径和颜色数组。其中颜色数组还用来确定同心圆的数量
class ConcentricPainter extends CustomPainter {
ConcentricPainter({this.minRadius = 10, this.maxRadius = 20,
this.colorsList = const [Colors.amber, Colors.cyan]});
final double minRadius;
final double maxRadius;
List<Color> colorsList = [Colors.amber, Colors.cyan];
final Paint mPaint = Paint()
..strokeWidth = 2
..style = PaintingStyle.stroke;
@override
void paint(Canvas canvas, Size size) {
debugPrint("size.width and size.height=${size.width } ${size.height }");
//将画布移到控件中间
canvas.translate(size.width / 2, size.height / 2);
Path path = Path();
mPaint.color = colorsList[0];
drawCircle(path, canvas, minRadius);
if (colorsList.length > 2) {
//被分割的同心圆份数
int count = colorsList.length - 1;
double divider = (maxRadius - minRadius) / count;
//从1开始,因为0是minRadius
for (int i = 1; i < count; i++) {
path.reset();
mPaint.color = colorsList[i];
drawCircle(path, canvas, minRadius + i * divider);
}
}
path.reset();
mPaint.color = colorsList[colorsList.length - 1];
drawCircle(path, canvas, maxRadius);
}
void drawCircle(Path path, Canvas canvas, double radius) {
path.addOval(Rect.fromCenter(center: Offset.zero, width: radius, height: radius));
canvas.drawPath(path, mPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
2.2 根据Slider来控制同心圆的中心点
调整同心圆的中心点,所以ConcentricPainter需要两个偏移量参数 centerFactorX和centerFactorY 因为需要Slider来控制中心点,所以需要创建一个State
class ConcentricPagerState extends State<ConcentricPager>{
//省略不重要代码
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (_, zone) {
debugPrint("zone maxWidth and zone maxHeight ${zone.maxWidth} ${zone.maxHeight}");
return Column(
children: [buildPager(), buildSliderX(), buildSliderY()],
);
});
}
Widget buildSliderX() {
return Padding(
padding: const EdgeInsets.all(30),
child: Slider(
max: 180,
min: -180,
divisions: 360,
value: _valueX,
onChanged: (v) {
setState(() {
_valueX = v;
});
}));
}
Widget buildSliderY() {
return Padding(
padding: const EdgeInsets.all(10),
child: Slider(
max: 180,
min: -180,
divisions: 360,
value: _valueY,
onChanged: (v) {
setState(() {
_valueY = v;
});
}));
}
Widget buildPager() {
return Container(
width: 300,
height: 200,
color: Colors.white,
alignment: Alignment.center,
child: LayoutBuilder(builder: (_, zone) {
return CustomPaint(
size: Size(zone.maxWidth, zone.maxHeight),
// 此时centerFactorX的值在-1到1之间
painter: ConcentricPainter(manage: manager,
minRadius: widget.minRadius, maxRadius: widget.maxRadius,
centerFactorX: _valueX / 180, centerFactorY: _valueY / 180),
);
}),
);
}
}
class ConcentricPainter extends CustomPainter {
ConcentricPainter({this.minRadius = 10, this.maxRadius = 20,
this.centerFactorX = 0, this.centerFactorY = 0, required this.manage})
: super(repaint: manage);
//省略不重要代码
void drawCircle(Path path, Canvas canvas, double radius, Size size) {
path.addOval(Rect.fromCenter
(center: Offset.zero + Offset(size.width / 2 * centerFactorX, size.height / 2 * centerFactorY),
width: radius, height: radius));
canvas.drawPath(path, mPaint);
}
}
到这里,一个可以调整中心点位置的同心圆控件就完成了
2.3 添加动画,动态改变边框颜色
前置知识,CustomPainter有一个类型为Listenable?的变量_repaint,Listenable是一个抽象类,一般其实现类是ChangeNotifier。当_repaint调用notifyListeners()时,CustomPainter就会重绘。 可以使用Ticker类对象可以实现16ms回调功能
class ConcentricManager with ChangeNotifier {
late List<Color> colorsList;
//原始颜色数组
late List<Color> originalColorList;
late DateTime datetime; // 用来上次更新时间
ConcentricManager(this.colorsList){
//对colorList数组进行深拷贝
originalColorList = List<Color>.generate(
colorsList.length,//要传入的长度,不能大于_categoryListModel.goods的长度,可根据实际需要设置
(int index){//创建新的QualitySamplingGoodsModel,默认系统会主动帮我们创建
return colorsList[index];
},growable: true);
datetime = DateTime.now();
}
//每秒会回调一次
void tick(DateTime now) {
randomColorList();
notifyListeners();
}
void randomColorList(){
int count = 0;
int size = colorsList.length;
Random random = Random();
List<Color> tempList = [];
//随机颜色变化
colorsList.forEach((element) {
int index = random.nextInt(size-1);
debugPrint("index:${index}");
tempList.add(originalColorList[index]);
count++;
});
colorsList = tempList;
}
}
class ConcentricPagerState extends State<ConcentricPager> with SingleTickerProviderStateMixin {
double _valueX = 20.0;
double _valueY = 20.0;
late Ticker _ticker;
late ConcentricManager manager;
@override
void initState(){
super.initState();
manager = ConcentricManager(widget.colorsList);
_ticker = createTicker(_tick)
..start();
}
//省略无关代码
void _tick(Duration elapsed) {
DateTime now = DateTime.now();
//1s更新一次
if(now.millisecondsSinceEpoch - manager.datetime.millisecondsSinceEpoch > 1000){
manager..datetime = now..tick(now);
}
}
至此初始版本的自定义控件功能便完成了
三,功能扩展
现在我们对此控件进行功能扩展,让它支持同心方形边框的绘制。 很明显方形同心边框和圆形同心边框只有绘制细节不同,其他都一样。所以我们可以把ConcentricPainter抽成抽象类,提供抽象方法让其子类实现
abstract class ConcentricPainter extends CustomPainter {
final ConcentricManager manage;
ConcentricPainter(
{this.minRadius = 10,
this.maxRadius = 20,
this.centerFactorX = 0,
this.centerFactorY = 0,
required this.manage})
: super(repaint: manage);
final double minRadius;
final double maxRadius;
final double centerFactorX;
final double centerFactorY;
List<Color> colorsList = [Colors.amber, Colors.cyan];
final Paint mPaint = Paint()
..strokeWidth = 2
..style = PaintingStyle.stroke;
@override
void paint(Canvas canvas, Size size) {
debugPrint("size.width and size.height=${size.width} ${size.height}");
canvas.translate(size.width / 2, size.height / 2);
Path path = Path();
colorsList = manage.colorsList;
mPaint.color = colorsList[0];
drawShape(path, canvas, minRadius, size);
if (colorsList.length > 2) {
//被分割的同心圆份数
int count = colorsList.length - 1;
double divider = (maxRadius - minRadius) / count;
//从1开始,因为0是minRadius
for (int i = 1; i < count; i++) {
path.reset();
mPaint.color = colorsList[i];
drawShape(path, canvas, minRadius + i * divider, size);
}
}
path.reset();
mPaint.color = colorsList[colorsList.length - 1];
drawShape(path, canvas, maxRadius, size);
}
void drawShape(Path path, Canvas canvas, double radius, Size size);
}
class ConcentricCirclePainter extends ConcentricPainter {
ConcentricCirclePainter(
{minRadius = 10,
maxRadius = 20,
centerFactorX = 0,
centerFactorY = 0,
required manage})
: super(
minRadius: minRadius,
maxRadius: maxRadius,
centerFactorX: centerFactorX,
centerFactorY: centerFactorY,
manage: manage);
@override
void drawShape(Path path, Canvas canvas, double radius, Size size) {
path.addOval(Rect.fromCenter(
center: Offset.zero +
Offset(size.width / 2 * centerFactorX,
size.height / 2 * centerFactorY),
width: radius,
height: radius));
canvas.drawPath(path, mPaint);
}
}
class ConcentricSquarePainter extends ConcentricPainter {
ConcentricSquarePainter(
{minRadius = 10,
maxRadius = 20,
centerFactorX = 0,
centerFactorY = 0,
required manage})
: super(
minRadius: minRadius,
maxRadius: maxRadius,
centerFactorX: centerFactorX,
centerFactorY: centerFactorY,
manage: manage);
@override
void drawShape(Path path, Canvas canvas, double radius, Size size) {
path.addRect(Rect.fromCenter(
center: Offset.zero +
Offset(size.width / 2 * centerFactorX,
size.height / 2 * centerFactorY),
width: radius,
height: radius));
canvas.drawPath(path, mPaint);
}
}
至此这个自定义控件的开发介绍就告一段落了。大家还可以在这个基础上开发更多功能,例如给控件增加淡入淡出的动画,或者扩展出更多不同图形的同心控件。学会了绘制技巧,就能把自己的想法通过绘制方式展示出来。希望大家能从此文中受益,从而开启绘制之旅,体会到绘制的乐趣。
感谢
您若喜欢,请点赞、关注,您的鼓励是我前进的动力
转载自:https://juejin.cn/post/7135424686619852831