likes
comments
collection
share

Flutter自定义控件初探

作者站长头像
站长
· 阅读数 53

前言

最近这段时间,在看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);
  }
}

至此这个自定义控件的开发介绍就告一段落了。大家还可以在这个基础上开发更多功能,例如给控件增加淡入淡出的动画,或者扩展出更多不同图形的同心控件。学会了绘制技巧,就能把自己的想法通过绘制方式展示出来。希望大家能从此文中受益,从而开启绘制之旅,体会到绘制的乐趣。

本文Demo请点击

感谢

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

转载自:https://juejin.cn/post/7135424686619852831
评论
请登录