移动小样

V1

2022/09/04阅读:16主题:默认主题

Flutter-自定义量角器

效果图

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

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

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

废话不多说,走起!

观察量角器

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

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,
    );
  }

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

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,
   );
  • 绘制一个圆弧

起始角度为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方法是一个角度到弧度的转换。绘制结果是这样的。

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

起始角度为正数:

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

起始角度为负数:

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

通过上面的了解,大概了解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);
  }
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);

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

执行2行代码:

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

上图就是执行完移动和旋转后坐标系的位置。这个时候我们可以绘制第一个刻度,也就是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,
    );

看到左下角那个红色横线了吗,这个就是0刻度线,那么1刻度线如何绘制呢?我们可以想下,如果我们想保持刚才绘制0刻度和1刻度的代码不变,是不是只要把这个半圆逆时针旋转1度就可以了,你想想是不是呢? 但是我们不好旋转半圆啊?怎么搞,反过来想,我们可以顺时针旋转画布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,
    );

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

  /*
   * 绘制刻度
   */
  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,
    );
  }

注意:上面代码中出现的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));
  }

这里主要是对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);
  }
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,
    );
  }

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

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);
  }

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

分类:

移动端开发

标签:

移动端开发

作者介绍

移动小样
V1