美文网首页Flutter
Flutter-自定义量角器

Flutter-自定义量角器

作者: 一笑轮回吧 | 来源:发表于2022-09-04 18:16 被阅读0次

    效果图

    image.png

    今天周末,收拾东西的时候,发现了一个尺子,我们叫它量角器,我记得上学的时候,借给一个女同学这样的量角器,到现在还没有还我,啥意思吗?还还我不?

    image.png

    看来是没戏了,同学吗?那么小气干嘛?不如自己画一个?毕竟我是最会编程的电工吗!

    image.png

    在自定义量角器之前,我先说下,小样就是小样,本着实现效果为目的,当然可能会有更好或者更优的方式,就像做数学题目一样,答案只有一个,但是解答思路有很多种,当然有好的建议和方式下面留言哦!

    废话不多说,走起!

    观察量角器

    拿起桌子上的量角器,看了看,想了想,欸?我还是不记得借我尺子的那个同学叫啥来着?


    image.png

    Sorry!

    这个量角器吗?有这几个特征。

    • 半圆形(里面有4个半圆)
    • 刻度线(长的、中等的、短的)
    • 有刻度值(正向,反向,注:这里0和180度省去,别问为啥,因为不好看)
    • 测量辅助线(10的倍数)

    像这种纯绘制的自定义基本上就是考验对Canvas API使用和数学知识,下面使用Flutter实现。

    具体实现

    创建Widget

    1、StatelessWidget Or StatefulWidget

    其实这个区分跟简单,当一个静态的,没有状态改变的自定义就使用StatelessWidget,否则使用StatefulWidget。因为尺子是一个静态的,一旦绘制完毕就不需要去改变了,所以说我们直接创建一个 StatelessWidget 就可以,

    //量角器Widget
    class SemiCircleRulerWidget extends StatelessWidget {
      const SemiCircleRulerWidget({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return ...
      }
    }
    
    2、尺寸如何定义

    我们刚开始一定会考虑这个宽高是如何定义,到底是直接外面传进来?还是怎么办?我这里直接采取使用LayoutBuilder,通过LayoutBuilder,我们可以在布局过程中拿到父组件传递的约束信息,然后我们可以根据约束信息动态的构建不同的布局。

    LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            double radius;
            //当宽的一半大于等于高的时候,采取高来作为半径。
            if (constraints.maxWidth / 2 >= constraints.maxHeight) {
              radius = constraints.maxHeight;
            }
            //当宽的一半小于高的时候,采取宽的一半作为半径。
            else {
              radius = constraints.maxWidth / 2;
            }
            //宽 = 2*半径, 高 = 半径
            var size = Size(radius * 2, radius);
            return CustomPaint(
              size: size,
              painter: SemiCircleRulerCustomPainter(radius),
            );
          },
        );
    

    这里简单做了一下判断,为了在已有空间里绘制最大

    • 当宽的一半大于等于高的时候,采取高来作为半径。
    • 当宽的一半小于高的时候,采取宽的一半作为半径。

    获取半径后,我们就可以为我们的CustomPaint设置大小了。

    • 宽 = 2*半径, 高 = 半径
    3、创建CustomPainter
    class SemiCircleRulerCustomPainter2 extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
         ...
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate)  => false;
    
    }
    

    shouldRepaint返回ture表示需要重绘,返回false表示不需要重绘。所以这里我们直接返回false就可以。

    4、paint方法
    4.1 坐标系

    有的时候,我们要是经常不写这种自定义的话,很容易忘记这个坐标系和画布在调用一些api之后的状态是啥样子的?所以我一般是这么做的,先写个绘制坐标系为了查看当时的坐标系情况。

     /*
       * 绘制坐标系( X轴  Y轴) 为了查看坐标系位置
       */
      void drawXY(Canvas canvas) {
        //X轴
        canvas.drawLine(
          const Offset(0, 0),
          const Offset(300, 0),
          Paint()
            ..color = Colors.green
            ..strokeWidth = 3,
        );
    
        //Y轴
        canvas.drawLine(
          const Offset(0, 0),
          const Offset(0, 300),
          Paint()
            ..color = Colors.red
            ..strokeWidth = 3,
        );
      }
    
    image.png

    简单绘制一个坐标系,为了更方便了解当前的画布情况。自定义完毕,删了即可。

    4.2 绘制量角器雏形

    量角器是一个半圆,所以我们需要调用的API是

    drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
    
    • rect:定义承载圆弧形状的矩形。通过设置该矩形可以指定圆弧的位置和大小。

    • startAngle: 设置圆弧是从哪个角度顺时针绘画的。顺时针为正,逆时针为负(注意:这里是弧度值)

    • sweepAngle: 设置圆弧顺时针扫过的角度。(注意:这里是弧度值)

    • useCenter: 绘制的时候是否使用圆心,我们绘制圆弧的时候设置为false,如果设置为true, 并且当前画笔的描边属性设置为Paint.Style.FILL的时候,画出的就是扇形。

    • paint: 指定绘制的画笔。

    为了更好了解这个API,我们来看下案例

    • 创建一个正方形
      Rect rect = const Rect.fromLTWH(100, 100, 300, 300);
      canvas.drawRect(
          rect,
          Paint()
            ..color = Colors.black
            ..style = PaintingStyle.stroke
            ..strokeWidth = 3,
       );
    
    image.png
    • 绘制一个圆弧

    起始角度为0:

      //绘制一个圆弧,从0度到90度
        var paint = Paint()
          ..color = Colors.red
          ..style = PaintingStyle.stroke
          ..strokeWidth = 3;
        canvas.drawArc(rect, degToRad(0), degToRad(90), false, paint);
        
          //角度转换为弧度
        double degToRad(num deg) => deg * (pi / 180.0);
    

    上面代码中degToRad方法是一个角度到弧度的转换。绘制结果是这样的。


    image.png

    通过效果我们可以知道起始0度是从水平开始的,扫描是顺时针扫描的。如果我们定义的起始不是0度呢?

    起始角度为正数:

    canvas.drawArc(rect, degToRad(30), degToRad(150), false, paint);
    
    image.png

    起始角度为负数:

     canvas.drawArc(rect, degToRad(-90), degToRad(180), false, paint);
    
    image.png

    通过上面的了解,大概了解drawArc绘制情况了。

    下面直接绘制量角器
      /*
       * 绘制表框(半圆)
       */
      void drawBorder(Canvas canvas, Size size) {
        Rect rect = Rect.fromCircle(
          center: Offset(radius, radius),
          radius: radius - borderStrokeWidth / 2,
        );
        //第四个参数设置为true,因为尺子是闭合的
        canvas.drawArc(rect, -pi, pi, true, borderPaint);
      }
    
    image.png
    4.3 绘制刻度线

    绘制线使用的API是:

     drawLine(Offset p1, Offset p2, Paint paint)
    

    2点确定一条直线,所以p1 p2就是2点的坐标,只要我们按照我们需要传入2个坐标即可,这里就不单独案例说明了,直接走起。

    • 定位:我想从半圆的左下角开始绘制刻度线。
    • 绘制刻度线:180个刻度,绘制每个刻度,其中10的倍数为大刻度,5结尾的刻度为中刻度,其他为小刻度,这里0和180度我们省略,因为绘制的话和圆弧底线重叠。
    定位

    这里说的定位,意思是操作画布来达到我想要绘制的起点,方便我绘制,因为我想在左下角开始绘制刻度线,所以,我执行下面的操作。

        //画布移动到(radius, radius)点
        canvas.translate(radius, radius);
        //画布旋转-90度
        canvas.rotate(degToRad(-90));
    

    这2个操作后画布变成啥样子了,看我们的坐标系就明白了。

    执行一行代码:

    canvas.translate(radius, radius);
    drawXY(canvas);
    
    image.png

    看到坐标系圆点移动到了(radius, radius)上了。

    执行2行代码:

        canvas.translate(radius, radius);
        canvas.rotate(degToRad(-90));
        drawXY(canvas);
    
    image.png

    上图就是执行完移动和旋转后坐标系的位置。这个时候我们可以绘制第一个刻度,也就是0度刻度线,如果我们需要绘制刻度线为20长的刻度线怎么做呢?

    通过坐标系我们可以知道,0刻度的起始位置 X轴是0,Y轴是-radius(为了好理解,这里面没有考虑画笔的宽度啊,后面绘制会考虑),如果我们要绘制20长度刻度的话,两点坐标可以为:

    • Offset(0.0, -(radius - borderStrokeWidth / 2))
    • Offset(0.0, -(radius - borderStrokeWidth / 2) + 20),
      这里borderStrokeWidth是画笔的宽度,因为要考虑画笔所以我们需要减去。
      canvas.drawLine(
          Offset(0.0, -(radius - borderStrokeWidth / 2)),
          Offset(0.0, -(radius - borderStrokeWidth / 2) + 20),
          scalePaint,
        );
    
    image.png

    看到左下角那个红色横线了吗,这个就是0刻度线,那么1刻度线如何绘制呢?我们可以想下,如果我们想保持刚才绘制0刻度和1刻度的代码不变,是不是只要把这个半圆逆时针旋转1度就可以了,你想想是不是呢?


    image.png

    但是我们不好旋转半圆啊?怎么搞,反过来想,我们可以顺时针旋转画布1刻度可以达到一样的效果。来试试

        //绘制0刻度
        canvas.drawLine(
          Offset(0.0, -(radius - borderStrokeWidth / 2)),
          Offset(0.0, -(radius - borderStrokeWidth / 2) + 20),
          scalePaint,
        );
    
        //顺时针旋转 1度
        canvas.rotate(degToRad(1));
        //查看坐标系
        drawXY(canvas);
    
        //绘制1刻度
        canvas.drawLine(
          Offset(0.0, -(radius - borderStrokeWidth / 2)),
          Offset(0.0, -(radius - borderStrokeWidth / 2) + 20),
          scalePaint,
        );
    
    image.png image.png

    如我们所希望的样子,呦西,趁势追击,直接一步到位。

      /*
       * 绘制刻度
       */
      void drawScale(Canvas canvas, Size size) {
        canvas.save();
        canvas.translate(radius, radius);
        canvas.rotate(degToRad(-90));
        for (int index = 1; index < 180; index++) {
          //旋转角度
          canvas.rotate(degToRad(1));
          //大刻度
          if (index % 10 == 0) {
            //绘制最长刻度
            drawLongLine(canvas, size);
          }
          //中刻度
          else if (index % 5 == 0) {
            //绘制中刻度
            drawMiddleLine(canvas, size);
          }
          //小刻度
          else {
            //绘制小刻度
            drawShortLine(canvas, size);
          }
        }
        canvas.restore();
      }
      
       /*
       * 绘制长线
       */
      void drawLongLine(Canvas canvas, Size size) {
        canvas.drawLine(
          Offset(0.0, -(radius - borderStrokeWidth / 2)),
          Offset(0.0, -(radius - borderStrokeWidth / 2) + longScaleSize),
          scalePaint,
        );
      }
    
      /*
       * 绘制中线
       */
      void drawMiddleLine(Canvas canvas, Size size) {
        canvas.drawLine(
          Offset(0.0, -(radius - borderStrokeWidth / 2)),
          Offset(0.0, -(radius - borderStrokeWidth / 2) + middleScaleSize),
          scalePaint,
        );
      }
    
      /*
       * 绘制短线
       */
      void drawShortLine(Canvas canvas, Size size) {
        canvas.drawLine(
          Offset(0.0, -(radius - borderStrokeWidth / 2)),
          Offset(0.0, -(radius - borderStrokeWidth / 2) + shortScaleSize),
          scalePaint,
        );
      }
    
    image.png

    注意:上面代码中出现的2行代码

    • canvas.save();
    • canvas.restore();

    这2位是成对出现,不能单独使用,他们的目的就是在你操作画布(平移,旋转等)之前,先调用save()方法对当前画布状态的保存,当你操作画布绘制好图形后,在调用restore()还原之前画布的状态,不影响后面绘制操作。

    4.3 绘制刻度值

    刻度值是在10的倍数才绘制,所以我们直接可以在绘制刻度线代码中,在绘制长刻度线的地方,多绘制刻度值即可,还有就是这里有2种刻度值,一种顺时针,一种逆时针,我们只要顺时针直接采用当前角度显示即可,逆时针使用(180-当前角度)即可。
    这里因为坐标系都是在对应位置,所以直接绘制就行。

      /*
       * 绘制数字
       */
      void drawScaleNum(Canvas canvas, int i) {
        //绘制最外圈刻度值
        textPainter.text = TextSpan(
            text: "$i",
            style: TextStyle(
              color: Colors.black,
              fontSize: numTextSize,
            ));
        textPainter.layout();
        double textStarPositionX = -textPainter.size.width / 2;
        double textStarPositionY = -radius + outNumSize;
        textPainter.paint(canvas, Offset(textStarPositionX, textStarPositionY));
    
        //绘制内圈刻度值
        textPainter.text = TextSpan(
            text: "${180 - i}",
            style: TextStyle(
              color: Colors.black,
              fontSize: numTextSize,
            ));
        textPainter.layout();
        double textStarPositionX2 = -textPainter.size.width / 2;
        double textStarPositionY2 = -radius + inNumSize;
        textPainter.paint(canvas, Offset(textStarPositionX2, textStarPositionY2));
      }
    
    image.png

    这里主要是对textPainter API的使用,这里主要1个注意点,就是2个刻度值的间距,通过控制y坐标来控制下就行,我这里 inNumSize=60,outNumSize=30,具体多少自己根据自己的审美修改就行,不做过多的解释。

    4.3 绘制内部半圆

    因为我们上面已经了解了绘制半圆的API,所以这里主要注意的点就是控制好半圆和刻度值的位置,避免重叠,其实也就是UI审美问题。

      /*
       * 绘制外半圆
       */
      void drawOuterSemicircle(Canvas canvas, Size size) {
        Rect rect = Rect.fromCircle(
          center: Offset(radius, radius),
          radius: radius - borderStrokeWidth / 2 - interSemicircleSize,
        );
        canvas.drawArc(rect, -pi, pi, true, semicirclePaint);
      }
    
      /*
       * 绘制内半圆
       */
      void drawInnerSemicircle(Canvas canvas, Size size) {
        Rect rect = Rect.fromCircle(
          center: Offset(radius, radius),
          radius: radius - borderStrokeWidth / 2 - outerSemicircleSize,
        );
        canvas.drawArc(rect, -pi, pi, true, semicirclePaint);
      }
    
    image.png
    4.4 绘制角度测量辅助线

    角度测量辅助线也是10的倍数的刻度才绘制,所以也是在绘制刻度尺代码中绘制10刻度的if里加上绘制角度测量辅助线代码即可。辅助线起点:Offset(radius, radius),终点是:内半圆为结束,我们可以把每个半圆和刻度值距离边缘的距离定义成变量,方便后续其他地方使用。

      void drawScale(Canvas canvas, Size size) {
        ...
        for (int index = 1; index < 180; index++) {
          ...
          //大刻度
          if (index % 10 == 0) {
            ...
            // 绘制刻度线
            drawScaleLine(canvas, size);
          }
          ...
        }
        canvas.restore();
      }
      
      /*
       * 绘制刻度线(10倍数)
       */
      void drawScaleLine(Canvas canvas, Size size) {
        canvas.drawLine(
          const Offset(0, 0),
          Offset(0, -radius + scalePaintWidth + interSemicircleSize),
          scalePaint,
        );
      }
    
    image.png image.png

    什么鬼?不好看,下面那个辅助线起点太多时,导致比较的密集,所以我想优化下,在搞个小半圆给他盖住,不让别人知道你的丑。

    4.5 绘制小半圆遮住你的美

    我们直接画个半圆,并且使用PaintingStyle.fill类型盖住他,为了好看我在画个半圆作为边框,不愧是我啊。

      /*
       * 绘制最小的半圆
       */
      void drawSmallSemicircle(Canvas canvas, Size size) {
        //绘制半圆区域
        Rect rect = Rect.fromCircle(
          center: Offset(radius, radius - borderStrokeWidth / 2),
          radius: radius / 10,
        );
        //这里先绘制半圆边框
        canvas.drawArc(rect, -pi, pi, true, semicirclePaint);
        
        //绘制白色半圆
        semicirclePaint.color = Colors.white;
        semicirclePaint.style = PaintingStyle.fill;
        canvas.drawArc(rect, -pi, pi, true, semicirclePaint);
      }
    
    image.png

    搞定!文章书写不易,多多关注!


    image.png

    相关文章

      网友评论

        本文标题:Flutter-自定义量角器

        本文链接:https://www.haomeiwen.com/subject/ofymnrtx.html