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,否则为5var 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,否则为5var 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.rhkb.cn/news/191284.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【算法|动态规划 | 区间dp No.2】AcWing 1068.环形石子合并

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

内衣洗衣机和手洗哪个干净?好用的内衣洗衣机推荐

在日常生活中&#xff0c;我们的衣服不可避免地会沾染上各种细菌、毛发和污渍&#xff0c;将它们与贴身衣物混合清洗&#xff0c;很容易发生交叉感染&#xff0c;而被感染后&#xff0c;贴身衣物也有可能导致我们人体引起皮肤病。这也是为什么大部分人都喜欢用手洗的原因&#…

Android WebView专题

WebView 专题 第一个WebView程序&#xff1a;加载远程网址 Layout添加WebView组件&#xff1b; <WebViewandroid:id"id/webView_first"android:layout_width"match_parent"android:layout_height"match_parent"/>初始化组件&#xff0c;加…

Socket网络编程(服务端和客户端代码示例)

本文主要讲解Socket网络编程。 首先介绍socket&#xff0c;包括TCP和UDP通信过程&#xff1b;然后介绍常用的函数&#xff1b;最后编写client-server例子&#xff0c;并进行测试。 文章目录 Socket介绍TCP通信过程服务器端通信过程&#xff1a;客户端通信过程&#xff1a; UDP通…

SA实战 ·《SpringCloud Alibaba实战》第13章-服务网关:项目整合SpringCloud Gateway网关

大家好,我是冰河~~ 一不小心[SpringCloud Alibaba实战》专栏都更新到第13章了,再不上车就跟不上了,小伙伴们快跟上啊! 在《SpringCloud Alibaba实战》专栏前面的文章中,我们实现了用户微服务、商品微服务和订单微服务之间的远程调用,并且实现了服务调用的负载均衡。也基于…

FusionDiff:第一个基于扩散模型实现的多聚焦图像融合的论文

文章目录 1. 论文介绍2. 研究动机3. 模型结构3.1 网络架构3.2 前向扩散过程3.3 逆向扩散过程3.4 训练和推理过程 4. 小样本学习4. 实验结果 1. 论文介绍 题目&#xff1a;FusionDiff: Multi-focus image fusion using denoising diffusion probabilistic models 作者&#xf…

【Mysql系列】Mysql基础篇

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Smart Link 和 Monitor Link应用

定义 Smart Link常用于双上行链路组网&#xff0c;提高接入的可靠性。 Monitor Link通过监视上行接口&#xff0c;使下行接口同步上行接口状态&#xff0c;起到传递故障信息的作用。 Smart Link&#xff0c;又叫做备份链路。一个Smart Link由两个接口组成&#xff0c;其中一个…

医用污水处理一体化设备怎么选

选择医用污水处理一体化设备时&#xff0c;可以从以下几个方面进行考虑&#xff1a; 设备材质&#xff1a;选择耐腐蚀、耐磨损、抗老化的材质&#xff0c;例如不锈钢、玻璃钢等。同时要确保设备罐体的抗压性能。工艺流程&#xff1a;选择高效、稳定、安全的工艺流程&#xff0…

智慧渔业捕捞计数项目设计书

&#xff08;一&#xff09;项目背景 根据捕捞水域的不同&#xff0c;我国水产捕捞可划分为海洋捕捞、远洋捕捞以及淡水捕捞三大类型。其中&#xff0c;淡水渔业主要是指在淡水水域进行捕捞、养殖以获得淡水水产品并对这些水产品进行加工的社会生产领域。 近年来&#xff0c;随…

vim相关命令讲解!

本文旨在讲解vim 以及其相关的操作&#xff01; 希望读完本文&#xff0c;读者会有一定的收获&#xff01;好的&#xff0c;干货马上就来&#xff01; 初识vim 在讲解vim之前&#xff0c;我们首先要了解vim是什么&#xff0c;有什么作用&#xff1f;只有了解了vim才能更好的理…

【Amazon】云上探索实验室—了解 AI 编程助手 Amazon Codewhisperer

文章目录 一、前言&#x1f4e2;二、关于云上探索实验室&#x1f579;️三、领学员需要做什么&#xff1f;✴️四、领学员能获得什么&#xff1f;&#x1f523;五、学课通道入口&#x1f447;1️⃣CSDN平台2️⃣网易云课堂3️⃣Skill Builder 平台 六、活动详情链接 一、前言&a…

详解IP安全:IPSec协议簇 | AH协议 | ESP协议 | IKE协议

目录 IP安全概述 IPSec协议簇 IPSec的实现方式 AH&#xff08;Authentication Header&#xff0c;认证头&#xff09; ESP&#xff08;Encapsulating Security Payload&#xff0c;封装安全载荷&#xff09; IKE&#xff08;Internet Key Exchange&#xff0c;因特网密钥…

Shell脚本 CPU,内存,磁盘占用率检测

CPU&#xff1a;运算资源占用 内存&#xff1a;RAM类介质 磁盘&#xff1a;ROM类介质 一、CPU #!/bin/bash# 设置阈值&#xff0c;当CPU占用超过该阈值时进行输出提示 threshold80while true do# 使用top命令获取CPU占用信息&#xff0c;并使用grep和awk筛选和解析输出结果…

开源会议通知H5页面邀请函制作源码系统+自动翻页 带完整的搭建教程

现如今&#xff0c;线上活动越来越频繁&#xff0c;而会议邀请函也成为了活动组织者不可或缺的工具。然而&#xff0c;传统的邮件、短信等方式发送邀请函已经无法满足现代人的需求。因此&#xff0c;开发一款现代化的、功能丰富的会议邀请函系统势在必行。下面源码小编将来给大…

Wireshark学习 与 TCP/IP协议分析

Wireshark简介和工具应用 如何开始抓包&#xff1f; 打开wireshark&#xff0c;显示如下网络连接。选择你正在使用的&#xff0c;&#xff08;比如我正在使用无线网上网&#xff09;&#xff0c;双击 可以先看下自己的ip地址和网关ip地址&#xff08;看抓包数据时候会用到&…

【React】Antd 组件基本使用

Antd 组件基本使用 第一步 安装并引入 antd 包 使用命令下载这个组件库 yarn add antd在我们需要使用的文件下引入&#xff0c;我这里是在 App.jsx 内引入 import { Button } from antd现在我们可以在 App 中使用 Button 组件 <div>App..<Button type"prima…

基于Mahony互补滤波的IMU数据优化_学习笔记整理

这周自己被安排进行优化软件 IMU 姿态解算项目&#xff0c;之前自己只简单了解四元数&#xff0c;对IMU数据处理从未接触&#xff0c;通过这一周的学习感觉收获颇丰&#xff0c;在今天光棍节之际&#xff0c;&#xff0c;&#xff0c;用大半天的时间对这一周的收获进行整理&…

程序员刚毕业找工作,选大厂和小厂有什么区别

前言 **关于应届生毕业之后应该去大厂好还是小厂好&#xff0c;一直都是被热议的话题之一。**对于刚毕业的应届生来说是一个特别难的问题&#xff0c;怕选择错会后悔。同时周围的亲戚朋友也各抒己见&#xff1a;‘’一定要去大公司&#xff01;大公司名气响&#xff0c;在那里…

Amazon Bedrock | 大语言模型CLAUDE 2体验

这场生成式AI与大语言模型的饥饿游戏&#xff0c;亚马逊云科技也参与了进来。2023年&#xff0c;亚马逊云科技正式发布了 Amazon Bedrock&#xff0c;是客户使用基础模型构建和扩展生成式AI应用程序的最简单方法&#xff0c;为所有开发者降低使用门槛。在 Bedrock 上&#xff0…