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

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进行绘制
      ),
    );
  }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

大数据毕业设计选题推荐-污水处理大数据平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

【数据结构】Lambda

⭐ 作者&#xff1a;小胡_不糊涂 &#x1f331; 作者主页&#xff1a;小胡_不糊涂的个人主页 &#x1f4c0; 收录专栏&#xff1a;浅谈数据结构 &#x1f496; 持续更文&#xff0c;关注博主少走弯路&#xff0c;谢谢大家支持 &#x1f496; Lambda表达式 1. 背景1.1 语法1.2 函…

【C++优先队列使用】问题总结

说明&#xff1a; 文章内容为关于priority_queue的使用总结&#xff0c;在C中要包含头文件<queue>文章内容为个人的学习整理&#xff0c;如有错误&#xff0c;欢迎指正。 文章目录 1. 优先队列默认是大根堆2. 关于优先队列和sort的比较逻辑2.1 sort的比较逻辑2.2 优先队…

python操作链接数据库和Mysql中的事务在python的处理

python操作数据库 pymysql模块: pip install pymysql作用:可以实现使用python程序链接mysql数据库&#xff0c;且可以直接在python中执行sql语句 添加操作 import pymysql #1.创建链接对象c conn pymysql.Connect(host127.0.0.1,#数据库服务器主机地址port3306, #mysql的端口…

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

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

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

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

Leetcode-206 反转链表

迭代法&#xff1a;将指针方向依次改变&#xff0c;定义两个指针pre和cur /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, Lis…

Bengio担任一作,联手一众图灵奖得主,预防AI失控,扛起AI监管大旗

图灵奖得主最近都在关心些什么呢&#xff1f;Yoshua Bengio&#xff0c;深度学习的奠基人之一&#xff0c;前几天他担任一作&#xff0c;联合多位大佬&#xff0c;发文探讨了如何在人工智能&#xff08;AI&#xff09;快速发展的时代管控相关风险&#xff0c;共同寻求当下生成式…

Flink SQL -- 命令行的使用

1、启动Flink SQL 首先启动Flink的集群&#xff0c;选择独立集群模式或者是session的模式。此处选择是时session的模式&#xff1a;yarn-session.sh -d 在启动Flink SQL的client&#xff1a; sql-client.sh 2、kafka SQL 连接器 在使用kafka作为数据源的时候需要上传jar包到…

python+requests接口自动化测试

原来的web页面功能测试转变成接口测试&#xff0c;之前大多都是手工进行&#xff0c;利用postman和jmeter进行的接口测试&#xff0c;后来&#xff0c;组内有人讲原先web自动化的测试框架移驾成接口的自动化框架&#xff0c;使用的是java语言&#xff0c;但对于一个学java&…

Linux学习之进程三

目录 进程控制 fork函数 什么是写时拷贝 进程终止 mian函数的返回值 退出码 错误码 exit() 进程等待 1.什么是进程等待&#xff1f; 2.为什么要进行进程等待&#xff1f; 3.如何进程进程等待&#xff1f; wait&#xff0c;waitpid&#xff1a; waitpid 进程替换 …

Lua更多语法与使用

文章目录 目的错误处理元表和元方法垃圾回收协程模块面向对象总结 目的 在前一篇文章&#xff1a; 《Lua入门使用与基础语法》 中介绍了一些基础的内容。这里将继续介绍Lua一些更多的内容。 同样的本文参考自官方手册&#xff1a; https://www.lua.org/manual/ 错误处理 下…

node插件MongoDB(四)—— 库mongoose 操作文档使用(新增、删除、更新、查看文档)(二)

文章目录 前言&#xff08;1&#xff09;问题&#xff1a;安装的mongoose 库版本不应该过高导致的问题&#xff08;2&#xff09;重新安装低版本 一、插入文档1. 代码2. node终端效果3. 使用mongo.exe查询数据库的内容 二、删除文档1. 删除一条2. 批量删除3. 代码 三、修改文档…

Go基础知识全面总结

文章目录 go基本数据类型bool类型数值型字符字符串 数据类型的转换运算符和表达式1. 算数运算符2.关系运算符3. 逻辑运算符4. 位运算符5. 赋值运算符6. 其他运算符运算符优先级转义符 go基本数据类型 bool类型 布尔型的值只可以是常量 true 或者 false。⼀个简单的例⼦&#…

MIPSsim模拟器 使用说明

&#xff08;一&#xff09; 启动模拟器 双击MIPSsim.exe&#xff0c;即可启动该模拟器。模拟器启动时&#xff0c;自动将自己初始化为默认状态。所设置的默认值为&#xff1a; u所有通用寄存器和浮点寄存器为全0&#xff1b; u内存清零&#xff1b; u流水寄存器为全0&#xff…

C++结构体定义 创建 赋值 结构体数组 结构体指针 结构体嵌套结构体

结构体是什么&#xff1f; struct是自定义数据类型&#xff0c;是一些类型集合组成的一个类型。结构体的定义方式 #include<iostream> using namespace std;struct Student {string name;int age;int score; };创建结构体变量并赋值 方式一&#xff0c;先创建结构体变…

基于springboot+vue开发的教师工作量管理系

教师工作量管理系 springboot31 源码合集&#xff1a;www.yuque.com/mick-hanyi/javaweb 源码下载&#xff1a;博主私 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了教师工作量管理系统的开发全过程。通过…

人工智能技术的高速发展,普通人如何借助AI实现弯道超车?

人工智能技术的高速发展&#xff0c;普通人如何借助AI实现弯道超车&#xff1f; 随着互联网信息传播的爆炸&#xff0c;人类科技文明的快速发展“人工智能”成为新的话题&#xff0c;科技的进步也让普通人觉得自己与社会脱节&#xff0c;找工作越来越难&#xff0c;创业越来越难…

Python使用Numba装饰器进行加速

Python使用Numba装饰器进行加速 前言前提条件相关介绍实验环境Numba装饰器进行加速未加速的代码输出结果 numba.jit加速的代码输出结果 前言 由于本人水平有限&#xff0c;难免出现错漏&#xff0c;敬请批评改正。更多精彩内容&#xff0c;可点击进入Python日常小操作专栏、Ope…

Aspose.OCR for .NET 2023Crack

Aspose.OCR for .NET 2023Crack 为.NET在图片上播放OCR使所有用户和程序员都可以从特定的图像片段中提取文本和相关的细节&#xff0c;如字体、设计以及书写位置。这一特定属性为OCR的性能及其在扫描遵循排列的记录时的功能提供了动力。OCR的库使用一条线甚至几条线来处理这些特…