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表盘的资料请关注我们其它相关文章!

(0)

相关推荐

  • js canvas仿支付宝芝麻信用分仪表盘

    这是一个仿支付宝芝麻信用分的一个canvas,其实就是一个动画仪表盘. 首先, 上原图: 这个是在下支付宝上的截图,分低各位见笑了.然后看下我用canvas实现的效果图: <canvas id="canvas" width="400" height="700" data-score='724'></canvas> <!-- 设置data-score,分数区间[400, 900] --> 唉,总感觉不像.这个是G

  • canvas绘制表盘时钟

    话不多说,请看代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>canvas绘制表盘</title> </head> <body> <canvas id='box' width="500" height="500" > 您的

  • 如何利用Flutter仿写微信搜索页效果

    目录 效果图 顶部搜索栏 SearchBar 实现细节 左边搜索框实现 右边取消按钮实现 内容的检索 内容的传递 内容的检索 搜索列表实现 总结 效果图 如上图所示,我们用 Flutter 来仿写搜索页面,这里聊天首页点击搜索栏会跳转到搜索页,搜索页面包含顶部搜索框跟底部 ListView,在搜索框内我们输入搜索词会检索聊天列表模型中 name 属性中包含搜索词的模型,并在底部列表中展示,且搜索词高亮显示.下面我们分别来介绍下这些功能的实现. 顶部搜索栏 class SearchBar exte

  • js canvas实现适用于移动端的百分比仪表盘dashboard

    本文为大家分享了canvas实现适用于移动端的百分比仪表盘,供大家参考,具体内容如下 由于最近工作中,经常会遇到一些动态百分比的仪表盘,一开始都是用图片样式方式实现: 但是随着越来越多的项目,决定用canvas绘制一个简易的仪表盘,便于以后项目中直接使用: 现版本只是书写为方法形式,也许之后会有时间对其优化为插件形式. 简简单单而已,以下直接给出代码和执行过程中的三张截图: <!doctype html> <html lang="en"> <head>

  • 用Flutter做桌上弹球(绘图(Canvas&CustomPaint)API)

    本文是Flutter中Canvas和CustomPaint API的使用实例. 首先看一下我们要实现的效果: 结合动图演示,列出最终目标如下: 在程序运行后,显示一个小球: 每次程序启动后,小球的样式均发生随机性变化,体现在大小.颜色和位置三点: 小球运行的规律参考桌球或三维弹球游戏: 单击屏幕,小球变色: 双击屏幕,小球暂停/恢复运动: 长按屏幕,小球开始/停止自动变色. 运用的主要技术点:Canvas和CustomPaint API. 运行平台:Android.iOS 源码地址: Githu

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

    目录 前言 初始化 面板 刻度 刻度线 刻度值 指针 时针 分针 秒针 动起来 前言 趁着周末空闲时间使用 Flutter 的 Canvas制作了一个精美表盘. 最终实现的效果还不错,如下: 前面说到使用 Canvas 实现该表盘效果,而在 Flutter 中使用 Canvas 更多的则是继承 CustomPainter 类实现 paint 方法,然后在 CustomPaint 中使用自定义实现的 CustomPainter. 比如这里创建的 DialPainter 使用如下: @overrid

  • 利用Matlab制作环形相册效果详解

    目录 运行效果 完整步骤 1.图片准备及导入 2.为每张图片制作遮罩层 3.调整每张图大小 4.绘图及绘图参数详解 完整代码 运行效果 完整步骤 1.图片准备及导入 要制作一款相册足够的图片量是必不可少的,不然整个相册只有一张图来回重复多没意思呀,因此我们需要一个文件夹专门放图片,为了方便导入,这里全部都是jpg格式: 图片导入代码: path='.\album\';%文件夹路径 files=dir(fullfile(path,'*.jpg')); picNum=size(files,1); %

  • Three.js利用顶点绘制立方体的方法详解

    前言 之前我们在学些WebGL基础的时候每天都是在一直研究顶点位置,法向量,绘制下标什么的.虽然复杂,但是毕竟原生,性能没得说. three.js也给我们提供了相关的接口供我们使用原生的方法绘制模型,下面话不多说了,来一起看看详细的介绍吧. 下面是我的个人一个案例. 首先,我创建了一个空白的形状: //立方体 var cubeGeometry = new THREE.Geometry(); 立方体的形状如下: // 创建一个立方体 // v6----- v5 // /| /| // v1----

  • Flutter利用Canvas模拟实现微信红包领取效果

    目录 前言 效果 红包弹出 红包布局 上半部分 下半部分 金币绘制 金币文字绘制 头像和文字 金币旋转 红包开启 结果弹出 前言 前面写了一篇Flutter利用Canvas绘制精美表盘效果详解的文章,对 Flutter 中的 Canvas 使用有了进一步的理解,就想着再用 Canvas 实现一个什么样的效果来加深一下对 Canvas 使用的理解,这个时候正好看到群里有人发红包,于是就想着能不能在 Flutter 中使用 Canvas 实现微信领取红包的效果?想到就做,知行合一,经过几天空余时间的

  • Android Flutter利用CustomPaint绘制基本图形详解

    目录 绘制矩形 绘制圆形 绘制椭圆 绘制任意形状 绘制弧形 总结 上一篇我们介绍了 CustomPaint 的基本概念和使用,可以看到 CustomPaint 其实和 前端的 Canvas基本上是一样的,实际上前端 Canvas 支持的绘制方法 CustomPaint 都支持,毕竟 CustomPaint 其实也是基于 Canvas 实现的.本篇我们来介绍 CustomPaint 基本图形的绘制. 绘制矩形 绘制矩形比较简单,方法定义如下: void drawRect(Rect rect, Pa

  • JavaScript利用canvas绘制流星雨效果

    目录 前言 需求分析 实现过程 1.绘制满天繁星 2.满天繁星闪起来 3.绘制流星 4.流星划过夜空 5.流星雨 6.merge视觉盛宴 前言 最近总是梦见一些小时候的故事,印象最深刻的就是夏天坐在屋顶上,看着满天的繁星,一颗,两颗,三颗...不由自主地开始了数星星的过程.不经意间,一颗流星划过夜间,虽然只是转瞬即逝,但它似乎比夜空中的其它繁星更吸引着我.听老人说,看见流星的时候许愿,愿望是可以实现的,此时早已把数星星抛之脑后,开始期待着下一颗流星的出现.但是那天晚上,流星再也没有出现,这也成了

  • 微信小程序利用canvas 绘制幸运大转盘功能

    小程序对 canvas api 跟h5的不太一致 ,所以这个搞的比较久,不多说,先贴代码 Page({ /** * 页面的初始数据 */ data: { awardsConfig: {}, restaraunts: [], //大转盘奖品信息 }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { var self = this; wx.getSystemInfo({ //获取系统信息成功,将系统窗口的宽高赋给页面的宽高 success:

  • Flutter 绘制风车实现示例详解

    目录 前言展示 1. 风车 1 的绘制 2. 风车 2 的绘制 3. 旋转动画的处理 4. 旋转动画的圈数 前言展示 最近源码看得比较多,本文来画点东西调节下心情,本绘制已收录于 FlutterUnit 的绘制集录,本文源码可参见[windmill.dart] .绘制内容非常简单,如下所示,两个样式的小风车:通过这两个小例子,可以学到: 路径的使用 画板的旋转变换 动画曲线与 Tween 的使用 风车1 风车2 1. 风车 1 的绘制 第一个风车非常简单,由四个 半圆 组成,每个部分直接的关系是

  • JavaScript+html5 canvas绘制的小人效果

    本文实例讲述了JavaScript+html5 canvas绘制的小人效果.分享给大家供大家参考,具体如下: 运行效果截图如下: index.html代码如下: <!DOCTYPE html> <html> <head> <meta http-equiv="Content-type" content="text/html;charset=UTF-8" /> <title>canvas中的缩放</tit

  • JS+Canvas绘制动态时钟效果

    本文实例为大家分享了Canvas绘制动态时钟效果展示的具体代码,供大家参考,具体内容如下 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> <style> #mycanvas{ position: absolute; left:50%; margin-left:-250px; border:5px solid #

随机推荐