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

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


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


这一期带来一点,简单、轻松又好玩的活,使用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 放在一个页面脚手架中

scala 复制代码
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 有状态的时钟类

scala 复制代码
class Clock extends StatefulWidget {
  const Clock({super.key});

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

3.3 时钟类的状态类

typescript 复制代码
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 绘图器类

ini 复制代码
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库,它提供了一些基本的数学函数和常量。需要单独导入:

arduino 复制代码
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' 库:

arduino 复制代码
import 'dart:async';

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

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

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

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

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

4. 效果展示

代码效果的 GIF 图展示如下:

F. 完整代码

scala 复制代码
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进行绘制
      ),
    );
  }
}
相关推荐
孤鸿玉11 小时前
Fluter InteractiveViewer 与ScrollView滑动冲突问题解决
flutter
叽哥18 小时前
Flutter Riverpod上手指南
android·flutter·ios
BG1 天前
Flutter 简仿Excel表格组件介绍
flutter
zhangmeng2 天前
FlutterBoost在iOS26真机运行崩溃问题
flutter·app·swift
恋猫de小郭2 天前
对于普通程序员来说 AI 是什么?AI 究竟用的是什么?
前端·flutter·ai编程
卡尔特斯2 天前
Flutter A GlobalKey was used multipletimes inside one widget'schild list.The ...
flutter
w_y_fan2 天前
Flutter 滚动组件总结
前端·flutter
醉过才知酒浓2 天前
Flutter Getx 的页面传参
flutter
李李记3 天前
别让 “断字” 毁了 Canvas 界面!splitByGrapheme 轻松搞定非拉丁文本换行
前端·canvas
火柴就是我3 天前
flutter 之真手势冲突处理
android·flutter