Flutter利用Canvas绘制精美表盘效果详解

前言

趁着周末空闲时间使用 flutter 的 canvas制作了一个精美表盘。

最终实现的效果还不错,如下:

前面说到使用 canvas 实现该表盘效果,而在 flutter 中使用 canvas 更多的则是继承 custompainter 类实现 paint 方法,然后在 custompaint 中使用自定义实现的 custompainter。 比如这里创建的 dialpainter 使用如下:

  @override
  widget build(buildcontext context) {
    double width = mediaquery.of(context).size.width;
    return container(
      color: const color.fromargb(255, 35, 36, 38), /// 设置背景
      child: center(
        child: custompaint( 
          size: size(width, width),
          painter: dialpainter(),
        ),
      ),
    );
  }

class dialpainter extends custompainter{
  @override
  void paint(canvas canvas, size size) {
  }

  @override
  bool shouldrepaint(covariant custompainter olddelegate) {
    return true;
  }
}

之后所有绘制的核心代码都在 dialpainter 中的 paint 中实现的,其中 shouldrepaint 是指父控件重新渲染时是否重新绘制,这里设置为 true 表示每次都重新绘制。

接下来就看具体实现代码,我们将整个表盘效果的实现分为三部分:面板刻度指针。涉及到的主要知识点包括:paintcanvaspathtextpainter 等。

初始化

在开始进行绘制之前,先进行画笔和长度单位的初始化。

在整个效果的实现上会多次使用到画笔 paint ,为了避免创建多个画笔实例,所以创建一个 paint 成员变量,后续通过修改其属性值来满足不同效果的绘制。

  late final paint _paint = _initpaint();

  paint _initpaint() {
    return paint()
      ..isantialias = true
      ..color = colors.white;
  }

通过初始化代码设置了画笔的抗锯齿和默认颜色。

为了方便后续使用长、宽、半径等长度,创建对应的成员变量,同时为了适配不同表盘宽高,保证展示效果一致,在绘制时不直接使用数值,而使用比例长度:

/// 画布宽度
late double width;
/// 画布高度
late double height;
/// 表盘半径
late double radius;
/// 比例单位长度
late double unit ;

@override
void paint(canvas canvas, size size) {
  initsize(size);
}

void initsize(size size) {
  width = size.width;
  height = size.height;
  radius = min(width, height) / 2;
  unit = radius / 15;
}

半径取宽度和高度的最小值,然后除以 2 ,单位长度 unit 取值为半径除以 15。

面板

首先绘制一个线性渐变的圆:

/// 绘制一个线性渐变的圆
var gradient = ui.gradient.linear(
  offset(width/2, height/2 - radius,),
  offset(width/2, height/2 + radius),
  [const color(0xfff9f9f9), const color(0xff666666)]);

_paint.shader = gradient;
_paint.color = colors.white;
canvas.drawcircle(offset(width/2, height/2), radius, _paint);

通过 gradient.linear 创建一个线性渐变颜色并设置给 paint.shader,绘制出来效果如下:

然后在其上添加一层径向渐变,增加表盘的立体感:

/// 绘制一层径向渐变的圆
var radialgradient = ui.gradient.radial(offset(width/2, height/2), radius, [
  const color.fromargb(216, 246, 248, 249),
  const color.fromargb(216, 229, 235, 238),
  const color.fromargb(216,205, 212, 217),
  const color.fromargb(216,245, 247, 249),
], [0, 0.92, 0.93, 1.0]);

_paint.shader = radialgradient;
canvas.drawcircle(offset(width/2, height/2), radius -  0.3 * unit, _paint);

使用 gradient.radial 创建一个径向渐变的颜色,效果如下:

最后再在表盘内添加一个边框和阴影增加对比效果:

/// 绘制 border
var shadowradius = radius -  0.8 * unit;
_paint
  ..color = const color.fromargb(33, 0, 0, 0)
  ..shader = null
  ..style = paintingstyle.stroke
  ..strokewidth = 0.1 * unit;
canvas.drawcircle(offset(width/2, height/2), shadowradius - 0.2 * unit, _paint);

///绘制阴影
path path = path();
path.moveto(width/2, height/2);
var rect = rect.fromltrb(width/2 - shadowradius, height/2 - shadowradius, width/2+shadowradius, height /2 +shadowradius);
path.addoval(rect);
canvas.drawshadow(path, const color.fromargb(51, 0, 0, 0), 1 * unit, true);

最后表盘效果如下:

刻度

面板绘制完成,接下来就是绘制刻度线以及刻度值。

刻度线

代码如下:

double dialcanvasradius = radius -  0.8 * unit;
canvas.save();
canvas.translate(width/2, height/2);

var y = 0.0;
var x1 = 0.0;
var x2 = 0.0;

_paint.shader = null;
_paint.color = const color(0xff929394);
for( int i = 0; i < 60; i++){
  x1 =  dialcanvasradius - (i % 5 == 0 ? 0.85 * unit : 1 * unit);
  x2 = dialcanvasradius - (i % 5 == 0 ? 2 * unit : 1.67 * unit);
  _paint.strokewidth = i % 5 == 0 ? 0.38 * unit : 0.2 * unit;
  canvas.drawline(offset(x1, y), offset(x2, y), _paint);
  canvas.rotate(2*pi/60);
}
canvas.restore();

表盘上有 60 个刻度,其中 12 个为小时刻度其余为分钟刻度,循环 60 次,通过 i % 5 == 0 判断是否为小时刻度,从而使用不同的 x 和 y 坐标,实现不同的长度和宽度。

这里为了避免去计算圆上的点坐标,采用的是旋转画布来实现。画布默认旋转点位左上角,所以需要通过 canvas.translate(width/2, height/2) 将旋转点移动到表盘的中心点,然后每绘制完一个刻度画布旋转 2*pi/60 的角度,即 6 度。因为画布进行了平移所以绘制的坐标都是基于圆中心,即相当于圆点移动到了圆中心。

最终实现刻度效果如图:

刻度值

绘制完刻度后需要给刻度标值,这里只显示 3、6、9、12 四个刻度值,代码如下:

double dialcanvasradius = radius -  0.8 * unit;
var textpainter = textpainter(
  text: const textspan(
    text:
    "3",
    style: textstyle(color: colors.black, fontsize: 20, fontweight: fontweight.bold, height: 1.0)),
  textdirection: textdirection.rtl,
  textwidthbasis: textwidthbasis.longestline,
  maxlines: 1,
)..layout();

var offset = 2.25 * unit;
var points = [
  offset(width / 2 + dialcanvasradius - offset - textpainter.width , height / 2 - textpainter.height / 2),
  offset(width / 2 - textpainter.width /2, height / 2 + dialcanvasradius - offset - textpainter.height),
  offset(width / 2 - dialcanvasradius + offset, height / 2 - textpainter.height / 2),
  offset(width / 2 - textpainter.width, height / 2 - dialcanvasradius + offset),
];
for(int i = 0; i< 4; i++){

  textpainter = textpainter(
    text: textspan(
      text:
      "${(i + 1) * 3}",
      style: const textstyle(color: colors.black, fontsize: 20, fontweight: fontweight.bold, height: 1.0)),
    textdirection: textdirection.rtl,
    textwidthbasis: textwidthbasis.longestline,
    maxlines: 1,
  )..layout();

  textpainter.paint(canvas, points[i]);
}

绘制文字使用的是 textpainter 对象,首先创建一个 textpainter 对象,用于测量获取文字的宽高,因为这里只显示 4 个刻度值,所以这里直接将对应需要绘制的坐标计算出来,然后循环绘制显示的刻度值在对应的位置即可。实现后效果如下:

指针

接下来就是指针的绘制,指针分为三部分:时针分针秒针。在绘制指针之前还需要绘制中心点:

var radialgradient =
  ui.gradient.radial(offset(width / 2, height / 2), radius, [
    const color.fromargb(255, 200, 200, 200),
    const color.fromargb(255, 190, 190, 190),
    const color.fromargb(255, 130, 130, 130),
  ], [0, 0.9, 1.0]);

/// 底部背景
_paint
  ..shader = radialgradient
  ..style = paintingstyle.fill;
canvas.drawcircle(
  offset(width/2, height/2), 2 * unit, _paint);


/// 顶部圆点
_paint
  ..shader = null
  ..style = paintingstyle.fill
  ..color = const color(0xff121314);
canvas.drawcircle(offset(width/2, height/2), 0.8 * unit, _paint);

代码很简单,在中心绘制两个圆,一个底部的径向渐变的大圆,一个顶部深色的小圆,如图:

时针

时针分为三部分,连接中心的矩形、连接矩形的半圆弧、最后的箭头,如图:

代码实现如下:

double hourhalfheight = 0.4 * unit;
double hourrectright =   7 * unit;

path hourpath = path();
/// 添加矩形 时针主体
hourpath.moveto(0 - hourhalfheight, 0 - hourhalfheight);
hourpath.lineto(hourrectright, 0 - hourhalfheight);
hourpath.lineto(hourrectright, 0 + hourhalfheight);
hourpath.lineto(0 - hourhalfheight, 0 + hourhalfheight);

/// 时针箭头尾部弧形
double offsettop = 0.5 * unit;
double arcwidth = 1.5 * unit;
double arrowwidth = 2.17 * unit;
double offset = 0.42 * unit;
var rect = rect.fromltwh(hourrectright - offset, 0 - hourhalfheight - offsettop, arcwidth, hourhalfheight * 2 + offsettop * 2);
hourpath.addarc(rect, pi/2, pi);
/// 时针箭头
hourpath.moveto(hourrectright - offset + arcwidth/2, 0 - hourhalfheight - offsettop);
hourpath.lineto(hourrectright - offset + arcwidth/2 + arrowwidth, 0);
hourpath.lineto(hourrectright - offset + arcwidth/2, 0 + hourhalfheight + offsettop);
hourpath.close();

canvas.save();
canvas.translate(width/2, height/2);
///绘制
_paint.color = const color(0xff232425);
canvas.drawpath(hourpath, _paint);
canvas.restore();

这里是通过 path 先添加一个矩形到路径,然后添加一个圆弧,圆弧向左偏移一定单位,防止对接效果不好,再添加一个三角形也就是箭头图形。这里所有的坐标计算都是基于圆点在圆盘的中心点计算的,所以需要平移画布,将圆点移动到圆盘的中心点,即 canvas.translate(width/2, height/2) 跟绘制表盘刻度的思路是一样的,最后再通过 canvas.drawpath 进行绘制。效果如下:

分针

分针的绘制相对比较简单,因为分针就一个圆角矩形,使用画布的 drawrrect 方法即可:

double hourhalfheight = 0.4 * unit;
double minutesleft = -1.33 * unit;
double minutestop = -hourhalfheight;
double minutesright = 11* unit;
double minutesbottom = hourhalfheight;

canvas.save();
canvas.translate(width/2, height/2);

/// 绘制分针
var rrect = rrect.fromltrbr(minutesleft, minutestop, minutesright, minutesbottom, radius.circular(0.42 * unit));
_paint.color = const color(0xff343536);
canvas.drawrrect(rrect, _paint);

canvas.restore();

实现思路同样是将画布移动到圆点,然后计算坐标进行绘制,这里需要注意的是分针尾部是超过了中心大圆点的,所以这里 left 需要向左偏移一定单位:

这里为了看到分针的效果,将时针隐藏掉了

秒针

秒针分为四部分:尾部弧形、尾部圆角矩形、细针、中心圆点:

实现代码:

double hourhalfheight = 0.4 * unit;
double secondsleft = -4.5 * unit;
double secondstop = -hourhalfheight;
double secondsright = 12.5 * unit;
double secondsbottom = hourhalfheight;

path secondspath = path();
secondspath.moveto(secondsleft, secondstop);

/// 尾部弧形
var rect = rect.fromltwh(secondsleft, secondstop, 2.5 * unit, hourhalfheight * 2);
secondspath.addarc(rect, pi/2, pi);

/// 尾部圆角矩形
var rrect = rrect.fromltrbr(secondsleft + 1 * unit, secondstop, - 2 * unit, secondsbottom, radius.circular(0.25 * unit));
secondspath.addrrect(rrect);

/// 指针
secondspath.moveto(- 2 * unit, - 0.125 * unit);
secondspath.lineto(secondsright, 0);
secondspath.lineto(-2 * unit, 0.125 * unit);

/// 中心圆
var ovalrect = rect.fromltwh(- 0.67 * unit, - 0.67 * unit, 1.33 * unit, 1.33 * unit);
secondspath.addoval(ovalrect);

canvas.save();
canvas.translate(width/2, height/2);

/// 绘制阴影
canvas.drawshadow(secondspath, const color(0xffcc0000), 0.17 * unit, true);

/// 绘制秒针
_paint.color = const color(0xffcc0000);
canvas.drawpath(secondspath, _paint);

canvas.restore();

思路跟时针的实现是一样的,通过 path 将圆弧、圆角矩形、三角形、中心圆形组合起来,计算坐标同样的是以圆盘中心为圆点,所有同样需要使用 translate 移动画布圆点后绘制。实现效果:

同样的为了更好的看到秒针的效果,将时针、分针隐藏了

动起来

经过上面的绘制,我们将表盘的所有元素都绘制出来了,但是最重要的没有动起来,动起来的关键就是要让时针、分针、秒针偏移一定的角度,既然是偏移角度自然就想到了旋转画布来实现,类似于绘制刻度一样。

分别在时针、分针、秒针的绘制之前对画布进行一定角度的旋转:

/// 时针
canvas.save();
canvas.translate(width/2, height/2);
canvas.rotate(2*pi/4);
_paint.color = const color(0xff232425);
canvas.drawpath(hourpath, _paint);
canvas.restore();

///分针
canvas.save();
canvas.translate(width/2, height/2);
canvas.rotate(2*pi/4*2);
var rrect = rrect.fromltrbr(minutesleft, minutestop, minutesright, minutesbottom, radius.circular(0.42 * unit));
_paint.color = const color(0xff343536);
canvas.drawrrect(rrect, _paint);
canvas.restore();

///秒针
canvas.save();
canvas.translate(width/2, height/2);
canvas.rotate(2*pi/4*3);
canvas.drawshadow(secondspath, const color(0xffcc0000), 0.17 * unit, true);
_paint.color = const color(0xffcc0000);
canvas.drawpath(secondspath, _paint);
canvas.restore();

分别在时针、分针、秒针的绘制前对画布旋转 90°、180°、270° ,效果如下:

通过画布旋转实现了我们想要的效果,接下来就是让指针根据时间旋转相应的角度。可以通过 datetime.now() 获取当前时间对象,进而获取当前的小时、分钟和秒。然后根据对应的值计算出相应的角度:

 var date = datetime.now();

/// 时针
canvas.rotate(2*pi/60*((date.hour - 3 + date.minute / 60 + date.second/60/60) * 5 ));

/// 分针
canvas.rotate(2*pi/60 * (date.minute - 15 + date.second / 60));

/// 秒针
canvas.rotate(2*pi/60 * (date.second - 15));

首先将 360 度分为 60 份,时针一小时为 5 份,因为角度的起始是在右侧中心点,所以获取的小时需要减 3,再加上分钟、秒钟占小时的比例;同理分别计算分钟、秒钟的角度,最终实现时针、分针、秒针根据当前时间展示。

角度计算对了以后,还需要刷新整个表盘,即每秒钟刷新一次,刷新时获取当前时间重新绘制时针、分针、秒针的位置,实现动态效果,这里使用 timer 每一秒钟调用父布局的 setstate 实现。

  @override
  void initstate() {
    super.initstate();

    timer.periodic(const duration(seconds: 1), (timer) {
      setstate(() {});
    });
  }

大功告成,最终实现了开始展示的表盘动态效果。

以上就是flutter利用canvas绘制精美表盘效果详解的详细内容,更多关于flutter canvas表盘的资料请关注www.887551.com其它相关文章!

(0)
上一篇 2022年3月23日
下一篇 2022年3月23日

相关推荐