Flutter笔记:绘图示例 - 一个简单的(Canvas )时钟应用

news2024/11/25 1:51:27
Flutter笔记
绘图示例 - 一个简单的(Canvas )时钟应用

作者李俊才 (jcLee95):https://blog.csdn.net/qq_28550263
邮箱 :291148484@163.com
本文地址:https://blog.csdn.net/qq_28550263/article/details/134341545


这一期带来一点,简单、轻松又好玩的活,使用Flutter绘图实现一个时钟应用。


1. 主要知识点介绍

  1. Flutter 绘图 :CustomPainter是一个可以在Canvas上进行自定义绘制的类。我们创建了一个ClockPainter类,继承自CustomPainter,并在paint方法中实现了时钟的绘制逻辑。

  2. Timer:这是一个可以在一定时间间隔后执行回调的类。我们使用Timer来每秒更新一次时钟的状态,从而实现指针的移动。

  3. DateTime:这是一个日期和时间的类,我们使用它来获取当前的时间。

  4. Paint:这是一个画笔的类,我们使用它来设置绘制时的颜色、笔触宽度等属性。

  5. Offset:这是一个表示二维向量的类,我们使用它来表示点的坐标。

2. 整体步骤

2.1 有状态时钟类 Clock

首先,我们创建了一个Clock类。它是一个StatefulWidget,因为我们需要一个可以改变状态的Widget来表示时钟。时钟的状态(当前时间)需要不断更新。

2.2 时钟类的状态类 _ClockState

在Clock类的状态类中,我们设置了一个每秒触发一次的定时器。每次定时器触发时,我们都会调用setState方法来更新状态,从而触发界面的重新绘制。

2.3 Flutter 绘图器类 ClockPainter -> CustomPainter

创建了一个继承自CustomPainter的ClockPainter类,用于在Canvas上进行自定义绘制。在ClockPainter的paint方法中,我们实现了时钟的绘制逻辑。接着:

  • 在paint方法中,我们首先绘制了时钟的表盘。我们使用了drawCircle方法来绘制一个圆形的表盘,然后使用了一个循环来绘制表盘上的刻度。

  • 接下来,我们绘制了时钟的指针。我们使用了DateTime类来获取当前的时间,然后根据当前的小时、分钟和秒数来计算指针的位置。我们使用了正弦和余弦函数来计算指针的位置,因为指针的移动可以看作是在单位圆上的旋转。

  • 最后,每当定时器触发时,我们都会更新当前的时间,并触发界面的重新绘制。在每次绘制时,我们都会根据当前的时间来绘制指针的位置,从而实现指针的移动。

2.4 放在一个页面脚手架中

class ClockPage extends StatelessWidget {
  const ClockPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('时钟'),
      ),
      body: const Center(
        child: Padding(
          padding: EdgeInsets.all(20),
          child: Clock(),
        ),
      ),
    );
  }
}

3. 代码实现

3.1 有状态的时钟类

class Clock extends StatefulWidget {
  const Clock({super.key});

  
  State<Clock> createState() => _ClockState();
}

3.3 时钟类的状态类

class _ClockState extends State<Clock> {
  late Timer _timer;

  
  void initState() {
    super.initState();
    _timer =
        Timer.periodic(const Duration(seconds: 1), (timer) => setState(() {})); // 每秒更新一次状态,重新绘制
  }

  
  void dispose() {
    _timer.cancel(); // 销毁时,取消定时器
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: 1,
      child: CustomPaint(
        painter: ClockPainter(DateTime.now()), // 使用自定义的ClockPainter进行绘制
      ),
    );
  }
}

3.3 绘图器类

class ClockPainter extends CustomPainter {
  final DateTime dateTime;

  ClockPainter(this.dateTime);

  
  void paint(Canvas canvas, Size size) {
    final centerX = size.width / 2; // 计算画布中心点的X坐标
    final centerY = size.height / 2; // 计算画布中心点的Y坐标
    final center = Offset(centerX, centerY); // 画布中心点
    final radius = min(centerX, centerY); // 计算画布的半径,取宽和高中的最小值

    final paint = Paint()..strokeWidth = 10; // 创建画笔,设置笔触宽度为10

    // 画表盘
    paint.color = Colors.black; // 设置画笔颜色为黑色
    paint.style = PaintingStyle.stroke; // 设置画笔样式为描边
    canvas.drawCircle(center, radius, paint); // 在画布上画一个圆形的表盘

    // 画刻度
    const tickWidth = 2.0; // 刻度线的宽度
    paint.strokeWidth = tickWidth; // 设置画笔宽度为刻度线的宽度
    for (var i = 0; i < 60; i++) { // 循环画60个刻度线
      var tickLength = i % 5 == 0 ? 15.0 : 5.0; // 如果是5的倍数,则刻度线长度为15,否则为5
      var tickX1 = centerX + radius * cos(i * 6 * pi / 180); // 计算刻度线起点的X坐标
      var tickY1 = centerY + radius * sin(i * 6 * pi / 180); // 计算刻度线起点的Y坐标
      var tickX2 = centerX + (radius - tickLength) * cos(i * 6 * pi / 180); // 计算刻度线终点的X坐标
      var tickY2 = centerY + (radius - tickLength) * sin(i * 6 * pi / 180); // 计算刻度线终点的Y坐标
      canvas.drawLine(Offset(tickX1, tickY1), Offset(tickX2, tickY2), paint); // 在画布上画刻度线
    }

    // 画时针
    final hourHandX = centerX +
        radius *
            0.4 *
            cos((dateTime.hour * 30 + dateTime.minute * 0.5) * pi / 180); // 计算时针的X坐标
    final hourHandY = centerY +
        radius *
            0.4 *
            sin((dateTime.hour * 30 + dateTime.minute * 0.5) * pi / 180); // 计算时针的Y坐标
    paint.color = Colors.red; // 设置画笔颜色为红色
    canvas.drawLine(center, Offset(hourHandX, hourHandY), paint); // 在画布上画时针

    // 画分针
    final minuteHandX = centerX +
        radius *
            0.6 *
            cos((dateTime.minute * 6 + dateTime.second * 0.1) * pi / 180); // 计算分针的X坐标
    final minuteHandY = centerY +
        radius *
            0.6 *
            sin((dateTime.minute * 6 + dateTime.second * 0.1) * pi / 180); // 计算分针的Y坐标
    paint.color = Colors.green; // 设置画笔颜色为绿色
    canvas.drawLine(center, Offset(minuteHandX, minuteHandY), paint); // 在画布上画分针

    // 画秒针
    final secondHandX =
        centerX + radius * 0.8 * cos((dateTime.second * 6) * pi / 180); // 计算秒针的X坐标
    final secondHandY =
        centerY + radius * 0.8 * sin((dateTime.second * 6) * pi / 180); // 计算秒针的Y坐标
    paint.color = Colors.blue; // 设置画笔颜色为蓝色
    canvas.drawLine(center, Offset(secondHandX, secondHandY), paint); // 在画布上画秒针
  }

  
  bool shouldRepaint(ClockPainter oldDelegate) {
    return dateTime != oldDelegate.dateTime; // 当时间改变时,重新绘制
  }
}

paint 方法中,首先计算了画布的中心点和半径。然后创建了一个 Paint 对象,用于设置绘制时的样式,如颜色、笔触宽度等。接下来,使用 drawCircle 方法绘制了表盘,然后通过一个循环绘制了 60 个刻度线。然后,根据当前的时间(dateTime)计算了时针、分针和秒针的位置,并使用 drawLine 方法将它们绘制到画布上。

shouldRepaint 方法决定了当新的 CustomPainter 对象与旧的 CustomPainter 对象比较时,是否需要重新绘制。在这个例子中,只有当时间改变时,才需要重新绘制,所以 shouldRepaint 方法返回了dateTime != oldDelegate.dateTime

中心点和半径

中心点是通过取画布宽度和高度的一半得到的。半径是画布宽度和高度中的最小值的一半。

刻度线的位置

我们使用了一个循环来绘制60个刻度线。每个刻度线的位置是通过计算其在单位圆上的角度得到的。我们使用了余弦(cos)和正弦(sin)函数来计算刻度线两端的坐标。这是因为单位圆上的点的坐标可以通过角度和半径来计算。

时针、分针和秒针的位置

我们使用了余弦和正弦函数来计算时针、分针和秒针的位置。这是因为指针的移动可以看作是在单位圆上的旋转。我们根据当前的时间(小时、分钟和秒)来计算指针的角度,然后使用余弦和正弦函数来计算指针的坐标。

  • 时针的角度是 (dateTime.hour * 30 + dateTime.minute * 0.5) * pi / 180。这是因为一小时对应30度(360度/12小时=30度),而一分钟对应0.5度(30度/60分钟=0.5度)。
  • 分针的角度是 (dateTime.minute * 6 + dateTime.second * 0.1) * pi / 180。这是因为一分钟对应6度(360度/60分钟=6度),而一秒对应0.1度(6度/60秒=0.1度)。
  • 秒针的角度是 (dateTime.second * 6) * pi / 180。这是因为一秒对应6度(360度/60秒=6度)。

注意,我们在计算角度时,需要将其从度转换为弧度,因为cos和sin函数接受的参数是弧度。我们通过乘以pi / 180来进行转换。

关于 math 库

在这段代码中,我们使用了Dart的math库,它提供了一些基本的数学函数和常量。需要单独导入:

import 'dart:math';
  1. min函数:min函数接受两个参数,并返回其中的最小值。在这段代码中,我们使用min函数来计算画布的半径,它是画布宽度和高度中的最小值的一半。

  2. cos函数和sin函数:cos函数和sin函数是三角函数,它们接受一个角度(以弧度为单位)作为参数,并返回该角度的余弦值和正弦值。在这段代码中,我们使用cos函数和sin函数来计算时钟刻度线和指针的位置。

  3. pi常量:pi是一个表示圆周率π的常量。在这段代码中,我们使用pi常量来将角度从度转换为弧度,因为cos函数和sin函数接受的参数是弧度。

  4. 乘法和除法运算:我们使用了乘法运算(*)和除法运算(/)来进行一些基本的数学计算,如计算画布的中心点和半径,计算刻度线和指针的位置等。

关于 Timer

Timer是Dart的dart:async库中的一个类,它可以在给定的持续时间(Duration)之后,或者每隔给定的持续时间,触发一个回调函数。使用 Timer 需要导入 ‘dart:async’ 库:

import 'dart:async';

在这个Flutter时钟应用中,我们使用了Timer的periodic构造函数来创建一个周期性的定时器。这个定时器每隔一秒(Duration(seconds: 1))就会触发一个回调函数。

这个回调函数是一个匿名函数,它调用了setState方法来更新状态。这会触发界面的重新绘制,从而更新时钟的显示。

当我们不再需要定时器时,我们可以调用cancel方法来取消定时器。在这个应用中,我们在dispose方法中调用了cancel方法,以确保当Widget被销毁时,定时器也被取消。例如

Timer _timer = Timer.periodic(Duration(seconds: 1), (timer) {
  // 这个回调函数会在每隔一秒时被触发
  print('Timer ticked!');
});

// 当我们不再需要定时器时,我们可以取消它
_timer.cancel();

4. 效果展示

代码效果的 GIF 图展示如下:
在这里插入图片描述

F. 完整代码

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';

class ClockPage extends StatelessWidget {
  const ClockPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Canvas 时钟'),
      ),
      body: const Center(
        child: Padding(
          padding: EdgeInsets.all(20),
          child: Clock(),
        ),
      ),
    );
  }
}

class ClockPainter extends CustomPainter {
  final DateTime dateTime;

  ClockPainter(this.dateTime);

  
  void paint(Canvas canvas, Size size) {
    final centerX = size.width / 2; // 计算画布中心点的X坐标
    final centerY = size.height / 2; // 计算画布中心点的Y坐标
    final center = Offset(centerX, centerY); // 画布中心点
    final radius = min(centerX, centerY); // 计算画布的半径,取宽和高中的最小值

    final paint = Paint()..strokeWidth = 10; // 创建画笔,设置笔触宽度为10

    // 画表盘
    paint.color = Colors.black; // 设置画笔颜色为黑色
    paint.style = PaintingStyle.stroke; // 设置画笔样式为描边
    canvas.drawCircle(center, radius, paint); // 在画布上画一个圆形的表盘

    // 画刻度
    const tickWidth = 2.0; // 刻度线的宽度
    paint.strokeWidth = tickWidth; // 设置画笔宽度为刻度线的宽度
    for (var i = 0; i < 60; i++) {
      // 循环画60个刻度线
      var tickLength = i % 5 == 0 ? 15.0 : 5.0; // 如果是5的倍数,则刻度线长度为15,否则为5
      var tickX1 = centerX + radius * cos(i * 6 * pi / 180); // 计算刻度线起点的X坐标
      var tickY1 = centerY + radius * sin(i * 6 * pi / 180); // 计算刻度线起点的Y坐标
      var tickX2 = centerX +
          (radius - tickLength) * cos(i * 6 * pi / 180); // 计算刻度线终点的X坐标
      var tickY2 = centerY +
          (radius - tickLength) * sin(i * 6 * pi / 180); // 计算刻度线终点的Y坐标
      canvas.drawLine(
          Offset(tickX1, tickY1), Offset(tickX2, tickY2), paint); // 在画布上画刻度线
    }

    // 画时针
    final hourHandX = centerX +
        radius *
            0.4 *
            cos((dateTime.hour * 30 + dateTime.minute * 0.5) *
                pi /
                180); // 计算时针的X坐标
    final hourHandY = centerY +
        radius *
            0.4 *
            sin((dateTime.hour * 30 + dateTime.minute * 0.5) * pi / 180);
    paint.color = Colors.red; // 设置画笔颜色为红色
    canvas.drawLine(center, Offset(hourHandX, hourHandY), paint); // 在画布上画时针

    // 画分针
    final minuteHandX = centerX +
        radius *
            0.6 *
            cos((dateTime.minute * 6 + dateTime.second * 0.1) *
                pi /
                180); // 计算分针的X坐标
    final minuteHandY = centerY +
        radius *
            0.6 *
            sin((dateTime.minute * 6 + dateTime.second * 0.1) *
                pi /
                180); // 计算分针的Y坐标
    paint.color = Colors.green; // 设置画笔颜色为绿色
    canvas.drawLine(center, Offset(minuteHandX, minuteHandY), paint); // 在画布上画分针

    // 画秒针
    final secondHandX = centerX +
        radius * 0.8 * cos((dateTime.second * 6) * pi / 180); // 计算秒针的X坐标
    final secondHandY = centerY +
        radius * 0.8 * sin((dateTime.second * 6) * pi / 180); // 计算秒针的Y坐标
    paint.color = Colors.blue; // 设置画笔颜色为蓝色
    canvas.drawLine(center, Offset(secondHandX, secondHandY), paint); // 在画布上画秒针
  }

  
  bool shouldRepaint(ClockPainter oldDelegate) {
    return dateTime != oldDelegate.dateTime; // 当时间改变时,重新绘制
  }
}

class Clock extends StatefulWidget {
  const Clock({super.key});

  
  State<Clock> createState() => _ClockState();
}

class _ClockState extends State<Clock> {
  late Timer _timer;

  
  void initState() {
    super.initState();
    _timer = Timer.periodic(const Duration(seconds: 1),
        (timer) => setState(() {})); // 每秒更新一次状态,重新绘制
  }

  
  void dispose() {
    _timer.cancel(); // 销毁时,取消定时器
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: 1,
      child: CustomPaint(
        painter: ClockPainter(DateTime.now()), // 使用自定义的ClockPainter进行绘制
      ),
    );
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1193771.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

kubernetes istio

目录 一、部署 二、部署示例应用 三、部署遥测组件 四、流量管理 五、熔断 官网&#xff1a;https://istio.io/latest/zh/about/service-mesh/ 一、部署 提前准备好文件 tar zxf 15t10-1.19.3-linux-amd64.tar.gz cd 15t10-1.19.3/ export PATH$PWD/bin:$PATHistioctl install …

一篇文章让你了解Java中的继承

目录 继承一.什么是继承二.为什么要使用继承三.继承的语法四.继承中有重复怎么办&#xff1f;1.**访问原则** 五.super和this1.**this**2.**super**3.**super注意事项**4.**super和this异同点**六.构造方法的引入1.父类不带参数的构造方法2.父类带有参数的构造方法 七.继承中的…

基于Docker容器DevOps应用方案

文章目录 基于docker容器DevOps应用方案环境基础配置1.所有主机永久关闭防火墙和selinux2.配置yum源3.docker的安装教程 配置主机名与IP地址解析部署gitlab.server主机1.安装gitlab2.配置gitlab3.破解管理员密码4.验证web页面 部署jenkins.server主机1.部署tomcat2.安装jenkins…

计算机技术专业CSIT883系统分析与项目管理介绍

文章目录 前言一、学科学习成果二、使用步骤三、最低出勤要求四、讲座时间表五、项目管理 前言 本课程介绍了信息系统开发中的技术和技术&#xff0c;以及与管理信息技术项目的任务相关的方法和过程。 它研究了系统分析师、客户和用户在系统开发生命周期中的互补角色。 它涵盖…

基于SSM的中学课内小说阅读与学习系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

关于Maven中pom.xml文件不报错但无法导包解决方法

问题 我的pom文件没有报红&#xff0c;但是依赖无法正常导入。 右下角还总出现这种问题。 点开查看报错日志。大致如下 1) Error injecting constructor, java.lang.NoSuchMethodError: org.apache.maven.model.validation.DefaultModelValidator: method <init>()V no…

【数据结构】树与二叉树(九):二叉树的后序遍历(非递归算法NPO)

文章目录 5.2.1 二叉树二叉树性质引理5.1&#xff1a;二叉树中层数为i的结点至多有 2 i 2^i 2i个&#xff0c;其中 i ≥ 0 i \geq 0 i≥0。引理5.2&#xff1a;高度为k的二叉树中至多有 2 k 1 − 1 2^{k1}-1 2k1−1个结点&#xff0c;其中 k ≥ 0 k \geq 0 k≥0。引理5.3&…

【考研数据结构代码题4】求树中度为1的结点数(递归方式)

题目&#xff1a;用C语言描述树的孩子兄弟链表结构&#xff0c;并编写递归程序求树中度为1的结点数 难度&#xff1a;★★ 算法思路&#xff1a;递归地遍历当前结点的左孩子子树与右兄弟子树&#xff0c;分别求二者中度为1的结点数记为h1,h2,若当前结点仅有1个结点&#xff0c;…

好用的vscode插件

一、代码管理 git GitLens — Git supercharged Git History gitignore 项目管理 Project Manager 管理多个项目 Todo Tree 快速定位代码中的todo WakaTime 用于在编程活动中自动统计工作量、代码提交和时间跟踪等 VS Code Counter 该插件用于帮助我们统计项目代码的行数…

防火墙部署模式 -- 单臂路由模式

防火墙单臂路由部署模式 如图&#xff0c;PC 1与PC 2通信&#xff0c;想在中间加上防火墙对其进行监控&#xff0c;并实现对两台设备的通信阻断&#xff0c;可以在交换机的另一个接口上连接防火墙&#xff0c;交换机将两台PC发送的数据引流到防火墙上&#xff0c;防火墙也配置下…

virtualBox虚拟机局域网访问配置

在VirtualBox中&#xff0c;桥接网络是一种网络连接类型&#xff0c;它允许虚拟机连接到物理网络上的路由器或交换机&#xff0c;在物理网络上获得独立的网络地址和访问权限。 一、设置VirtualBox桥接网络的步骤&#xff1a; 打开VirtualBox软件&#xff0c;并选择你想要配置…

P1547 [USACO05MAR] Out of Hay S 题解

文章目录 题目描述输入格式输出格式样例样例输入样例输出 完整代码 题目描述 Bessie 计划调查 N N N&#xff08; 2 ≤ N ≤ 2 000 2 \leq N \leq 2\,000 2≤N≤2000&#xff09;个农场的干草情况&#xff0c;它从 1 1 1 号农场出发。农场之间总共有 M M M&#xff08; 1 ≤…

Matlab导出高清图片方法

一、背景 使用matlab绘制图片后&#xff0c;需要将图片导出为.jpg或.eps格式以便后期使用。但通过文件–另存为.jpg时&#xff0c;并没有清晰度选择&#xff0c;导出的图片只有30几k&#xff0c;以至于图片很模糊。 二、Matlab导出高清图片方法 文件—导出设置 1、大小&…

kubernetes etcd

目录 一、备份 二、回复 官网&#xff1a; https://v1-25.docs.kubernetes.io/zh-cn/docs/tasks/administer-cluster/configure-upgrade-etcd/#restoring-an-etcd-cluster 一、备份 从镜像中拷贝etcdctl二进制命令 输入ctrlpq快捷键&#xff0c;把容器打入后台 docker run…

opencv dnn模块 示例(22) 目标检测 object_detection 之 yolov7

在YOLOv6 初版出来不久&#xff0c;YOLOv7就立马横空出世了。与YOLOv5、YOLOv6不同&#xff0c;YOLOv7是由YOLOv4团队的原班人马提出的&#xff08;官方出品&#xff09;。从论文的表上来看&#xff0c;目前YOLOv7无论是在实时性还是准确率上都已经超过了当时已知的所有目标检测…

【二叉树】如何构建一个包含大量随机数节点的二叉树测试用例

【二叉树】如何构建一个包含大量随机数节点的二叉树测试用例 前言一、案例准备二、自动生成随机二叉树工具类&#xff08;TreegenerateUtils&#xff09;三、如何调用随机二叉树工具类&#xff08;TreegenerateUtils&#xff09;&#xff1f; 前言 今天笔者在测试有关二叉树的…

打开ps提示,计算机中丢失d3dcompiler_47.dll怎么解决?

“d3dcompiler_47.dll丢失5个解决办法”。相信很多同事在工作或者娱乐的过程中&#xff0c;都遇到过这个错误提示。那么&#xff0c;究竟什么是d3dcompiler_47.dll文件&#xff1f;为什么会丢失呢&#xff1f;又该如何解决这个问题呢&#xff1f;接下来&#xff0c;我将为大家详…

财税服务展示预约小程序的作用是什么

财税财政往往困扰着很多公司&#xff0c;尤其是公司里没有相应职员或工作压力大的情况下&#xff0c;不少商家就会寻找代理记账、审计服务、会计代理等服务的机构。 对财政服务代理机构&#xff08;会计公司&#xff09;来说&#xff0c;市场企业多而广&#xff0c;理论上来说…

yolov5 利用Labelimg对图片进行标注

首先打开yolov5-master&#xff0c;在data文件中新建一个文件夹来存放你需要跑的数据&#xff0c;例如我这次跑的是羽毛球&#xff0c;文件把文件取名为badminton。使用其他文件夹例如images也可以&#xff0c;就是跑多了以后不好整理&#xff0c;然后点击 选中刚刚你存放数据的…

【算法 | 数论 No.1】AcWing1246. 等差数列

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【AcWing算法提高学习专栏】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&a…