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

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

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


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});
  @override
  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});
  @override
  State<Clock> createState() => _ClockState();
}

3.3 时钟类的状态类

class _ClockState extends State<Clock> {
  late Timer _timer;
  @override
  void initState() {
    super.initState();
    _timer =
        Timer.periodic(const Duration(seconds: 1), (timer) => setState(() {})); // 每秒更新一次状态,重新绘制
  }
  @override
  void dispose() {
    _timer.cancel(); // 销毁时,取消定时器
    super.dispose();
  }
  @override
  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);
  @override
  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); // 在画布上画秒针
  }
  @override
  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});
  @override
  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);
  @override
  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); // 在画布上画秒针
  }
  @override
  bool shouldRepaint(ClockPainter oldDelegate) {
    return dateTime != oldDelegate.dateTime; // 当时间改变时,重新绘制
  }
}
class Clock extends StatefulWidget {
  const Clock({super.key});
  @override
  State<Clock> createState() => _ClockState();
}
class _ClockState extends State<Clock> {
  late Timer _timer;
  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(const Duration(seconds: 1),
        (timer) => setState(() {})); // 每秒更新一次状态,重新绘制
  }
  @override
  void dispose() {
    _timer.cancel(); // 销毁时,取消定时器
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: 1,
      child: CustomPaint(
        painter: ClockPainter(DateTime.now()), // 使用自定义的ClockPainter进行绘制
      ),
    );
  }
}
目录
相关文章
|
28天前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
114 4
|
29天前
|
缓存 监控 前端开发
优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面
本文探讨了优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面,并通过案例分析展示了具体措施和效果,强调了持续优化的重要性及未来优化方向。
54 10
|
1月前
|
存储 调度 数据安全/隐私保护
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
鸿蒙应用打包上架流程包括创建应用、打包签名和上传应用。首先,在AppGallery Connect中创建项目、APP ID和元服务。接着,使用Deveco进行手动签名,生成.p12和.csr文件,并在AppGallery Connect中上传CSR文件获取证书。最后,配置签名并打包生成.app文件,上传至应用市场。常见问题包括检查签名配置文件是否正确。参考资料:[应用/服务签名](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-signing-V5)。
63 3
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
|
28天前
|
传感器 前端开发 Android开发
在 Flutter 开发中,插件开发与集成至关重要,它能扩展应用功能,满足复杂业务需求
在 Flutter 开发中,插件开发与集成至关重要,它能扩展应用功能,满足复杂业务需求。本文深入探讨了插件开发的基本概念、流程、集成方法、常见类型及开发实例,如相机插件的开发步骤,同时强调了版本兼容性、性能优化等注意事项,并展望了插件开发的未来趋势。
40 2
|
1月前
|
存储 Dart
Flutter&鸿蒙next 实现一个计算器应用
本文介绍了如何使用 Flutter 创建一个简单的计算器应用,包括基本的加减乘除运算。文章详细讲解了界面布局、计算逻辑和状态管理的实现步骤,通过具体的代码示例展示了如何构建计算器界面、处理用户输入和显示计算结果。
89 0
|
Android开发
【错误记录】Android Studio 创建 Flutter 应用被卡住 ( 更新 Flutter 插件 | 命令行创建 | 断网 )
【错误记录】Android Studio 创建 Flutter 应用被卡住 ( 更新 Flutter 插件 | 命令行创建 | 断网 )
332 0
【错误记录】Android Studio 创建 Flutter 应用被卡住 ( 更新 Flutter 插件 | 命令行创建 | 断网 )
|
2月前
|
Android开发 iOS开发 容器
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
|
28天前
|
开发框架 Dart 前端开发
Flutter 是谷歌推出的一款高效跨平台移动应用开发框架,使用 Dart 语言,具备快速开发、跨平台支持、高性能、热重载及美观界面等特点。
Flutter 是谷歌推出的一款高效跨平台移动应用开发框架,使用 Dart 语言,具备快速开发、跨平台支持、高性能、热重载及美观界面等特点。本文从 Flutter 简介、特点、开发环境搭建、应用架构、组件详解、路由管理、状态管理、与原生代码交互、性能优化、应用发布与部署及未来趋势等方面,全面解析 Flutter 技术,助你掌握这一前沿开发工具。
56 8
|
28天前
|
存储 JavaScript 前端开发
在Flutter开发中,状态管理至关重要。随着应用复杂度的提升,有效管理状态成为挑战
在Flutter开发中,状态管理至关重要。随着应用复杂度的提升,有效管理状态成为挑战。本文介绍了几种常用的状态管理框架,如Provider和Redux,分析了它们的基本原理、优缺点及适用场景,并提供了选择框架的建议和使用实例,旨在帮助开发者提高开发效率和应用性能。
35 4
|
2月前
|
开发者
鸿蒙Flutter实战:07-混合开发
鸿蒙Flutter混合开发支持两种模式:1) 基于har包,便于主项目开发者无需关心Flutter细节,但不支持热重载;2) 基于源码依赖,利于代码维护与热重载,需配置Flutter环境。项目结构包括AppScope、flutter_module等目录,适用于不同开发需求。
103 3