flutter 手写时钟

news2024/9/22 17:26:05

前言:

之前看过别人写的 js实现的 时钟表盘 挺有意思的,看着挺好 这边打算自己手动实现以下。顺便记录下实现过程:大致效果如下:

主要技术点:

表盘内样
倒角:

        表盘下半部分是有一点倒角的感觉,实际是是两个 半径相差不多的圆,以上对齐的方式实现的。下面的圆稍微大点有个相对较深的颜色,然后上面在该一个白的圆。

表盘刻度内阴影:

        flutter 实际上是不支持 内阴影的。我们这里一个带阴影的圆 通过 ClipRRect 裁切的方式实现的

表盘刻度

        表盘的刻度主要是还是利用 正弦函数 和 余弦函数,已知圆的半径  来计算 圆上的一个点因为计算机的 0度实在 x轴方向。所以在 本例子里面 很多地方需要将起始角度 逆时针 旋转 -90 度。来对齐 秒针 和 分针 时针 的起始位置

 

小时
//数字时间
List<Positioned> _timeNum(Size s) {
  final List<Positioned> timeArray = [];
  //默认起始角度,默认为3点钟方向,定位到12点钟方向 逆时针 90度
  const double startAngle = -pi / 2;
  final double radius = s.height / 2 - 25;
  final Size center = Size(s.width / 2 - 5, s.height / 2 - 6);
  int angle;
  double endAngle;
  for (int i = 12; i > 0; i--) {
    angle = 30 * i;
    endAngle = ((2 * pi) / 360) * angle + startAngle;
    double x = center.width + cos(endAngle) * radius;
    double y = center.height + sin(endAngle) * radius;

    timeArray.add(
      Positioned(
        left: x - 5,
        top: y,
        child: Container(
          width: 20,
          // color: Colors.blue,
          child: Text(
            '$i',
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );
  }

  return timeArray;
}
表盘指针

        指针实际上是通过 ClipPath 来裁切一个 带颜色的 Container:已知 Container 大小,确定四个点的位置:起始点(0,0)位置  在 左上角

秒针的实现
Widget _pointerSecond() {
  return SizedBox(
    width: 120,
    height: 10,
    child: ClipPath(
      clipper: SecondPath(),
      child: Container(
        decoration: const BoxDecoration(
          color: Colors.red,
        ),
      ),
    ),
  );
}

  辅助秒针类:

class SecondPath extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    var path = Path();
    path.moveTo(size.width / 3, 0);
    path.lineTo(size.width, size.height / 2);
    path.lineTo(size.width / 3, size.height);
    path.lineTo(0, size.height / 2);
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) {
    return true;
  }
}
针动起来

        这里主要是通过 隐式动画 AnimatedRotation 只要修改他的旋转就能自己实现转动,传入一个 旋转的圈数,来实现移动的动画:

Center(
  child: Transform.rotate(
    angle: -pi / 2,
    child: AnimatedRotation(
      //圈数 1 >一圈, 0.5 半圈
      turns: _turnsSecond,
      duration:
          const Duration(milliseconds: 250),
      child: Padding(
        padding:
            const EdgeInsets.only(left: 30),
        child: _pointerSecond(),
      ),
    ),
  ),
),

这里有个小插曲是 圈数开始到下一圈的时候 要累加一个圈数进去,才能继续往顺时针 方向继续旋转

完整代码:

import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';

class PageTime extends StatefulWidget {
  const PageTime({Key? key}) : super(key: key);

  @override
  State<PageTime> createState() => _PageTimeState();
}

class _PageTimeState extends State<PageTime> {
  Timer? _timer;
  DateTime _dateTime = DateTime.now();
  int _timeSecond = 0;
  int _timeMinute = 0;
  int _timeHour = 0;
  //圈数
  int _turnSecond = 0;
  //圈数
  int _turnMinute = 0;
  //圈数
  int _turnHour = 0;

  ///秒的圈数
  double get _turnsSecond {
    if (_timeSecond == 0) {
      _turnSecond++;
    }
    return _turnSecond + _timeSecond / 60;
  }

  double get _turnsMinute {
    if (_timeMinute == 0) {
      _turnMinute++;
    }
    return _turnMinute + _timeMinute / 60;
  }

  double get _turnsHour {
    if (_timeHour % 12 == 0) {
      _turnHour++;
    }
    return _turnHour + (_timeHour % 12) / 12;
  }

  @override
  void initState() {
    // TODO: implement initState

    super.initState();
    _timeSecond = _dateTime.second;
    _timeMinute = _dateTime.minute;
    _timeHour = _dateTime.hour;

    _timer = Timer.periodic(
      const Duration(seconds: 1),
      (timer) {
        setState(() {
          _dateTime = DateTime.now();
          _timeSecond = _dateTime.second;
          _timeMinute = _dateTime.minute;
          _timeHour = _dateTime.hour;
        });
      },
    );
  }

  @override
  void dispose() {
    _timer?.cancel();
    // TODO: implement dispose
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blueGrey,
      appBar: AppBar(
        title: const Text('时钟'),
        centerTitle: true,
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // Container(
            //   height: 50,
            //   width: 180,
            //   color: Colors.white,
            //   child: Center(
            //       child: Text(
            //           '$_timeHour-$_timeMinute:$_timeSecond')),
            // ),
            Container(
              width: 260,
              height: 260,
              decoration: BoxDecoration(
                color: Colors.white70,
                borderRadius: BorderRadius.circular(130),
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withOpacity(0.3),
                    spreadRadius: 2,
                    blurRadius: 5,
                    offset: const Offset(0, 6), // 阴影的偏移量
                  ),
                ],
              ),
              child: Align(
                alignment: Alignment.topCenter,
                child: Container(
                  width: 255,
                  height: 255,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(255 / 2),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.black.withOpacity(0.3),
                        spreadRadius: 2,
                        blurRadius: 5,
                        offset: const Offset(0, 6), // 阴影的偏移量
                      ),
                    ],
                  ),
                  child: Center(
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(110),
                      child: Container(
                        width: 220,
                        height: 220,
                        decoration: BoxDecoration(
                          // color: Colors.transparent,
                          borderRadius: BorderRadius.circular(110),
                          gradient: RadialGradient(
                            colors: [
                              Colors.white,
                              Colors.black.withOpacity(0.2),
                            ],
                            stops: const [0.50, 1.0],
                            center: Alignment.center,
                            radius: 0.9, // 渐变的半径,从圆心到边缘
                          ),
                        ),
                        child: Stack(
                          children: [
                            ..._timeScale(const Size(220, 220)),
                            ..._timeNum(const Size(220, 220)),
                            Center(
                              child: Container(
                                width: 140,
                                height: 140,
                                // color: Colors.blue,
                                child: Stack(
                                  children: [
                                    Center(
                                      child: Transform.rotate(
                                        angle: -pi / 2,
                                        child: AnimatedRotation(
                                          turns: _turnsHour,
                                          duration: const Duration(seconds: 1),
                                          child: Padding(
                                            padding:
                                                const EdgeInsets.only(left: 30),
                                            child: _pointerHour(),
                                          ),
                                        ),
                                      ),
                                    ),
                                    Center(
                                      child: Transform.rotate(
                                        angle: -pi / 2,
                                        child: AnimatedRotation(
                                          turns: _turnsMinute,
                                          duration: const Duration(seconds: 1),
                                          child: Padding(
                                            padding:
                                                const EdgeInsets.only(left: 30),
                                            child: _pointerMinute(),
                                          ),
                                        ),
                                      ),
                                    ),
                                    Center(
                                      child: Transform.rotate(
                                        angle: -pi / 2,
                                        child: AnimatedRotation(
                                          //圈数 1 >一圈, 0.5 半圈
                                          turns: _turnsSecond,
                                          duration:
                                              const Duration(milliseconds: 250),
                                          child: Padding(
                                            padding:
                                                const EdgeInsets.only(left: 30),
                                            child: _pointerSecond(),
                                          ),
                                        ),
                                      ),
                                    ),
                                    Center(
                                      child: Container(
                                        width: 20,
                                        height: 20,
                                        decoration: BoxDecoration(
                                          color: Colors.white,
                                          boxShadow: [
                                            BoxShadow(
                                              color:
                                                  Colors.black.withOpacity(0.3),
                                              spreadRadius: 2,
                                              blurRadius: 5,
                                              offset: Offset(0, 6), // 阴影的偏移量
                                            ),
                                          ],
                                          borderRadius:
                                              BorderRadius.circular(10),
                                        ),
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                            )
                          ],
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 24),
              child: _pointerSecond(),
            ),
            _pointerMinute(),
            _pointerHour(),
          ],
        ),
      ),
    );
  }

  //数字时间
  List<Positioned> _timeNum(Size s) {
    final List<Positioned> timeArray = [];
    //默认起始角度,默认为3点钟方向,定位到12点钟方向 逆时针 90度
    const double startAngle = -pi / 2;
    final double radius = s.height / 2 - 25;
    final Size center = Size(s.width / 2 - 5, s.height / 2 - 6);
    int angle;
    double endAngle;
    for (int i = 12; i > 0; i--) {
      angle = 30 * i;
      endAngle = ((2 * pi) / 360) * angle + startAngle;
      double x = center.width + cos(endAngle) * radius;
      double y = center.height + sin(endAngle) * radius;

      timeArray.add(
        Positioned(
          left: x - 5,
          top: y,
          child: Container(
            width: 20,
            // color: Colors.blue,
            child: Text(
              '$i',
              textAlign: TextAlign.center,
            ),
          ),
        ),
      );
    }

    return timeArray;
  }

  //刻度时间
  List<Positioned> _timeScale(Size s) {
    final List<Positioned> timeArray = [];
    //默认起始角度,默认为3点钟方向,定位到12点钟方向
    // const double startAngle = -pi / 2;
    const double startAngle = 0;
    final double radius = s.height / 2 - 10;
    final Size center = Size(s.width / 2 - 3, s.height / 2 + 3);
    int angle;
    double endAngle;
    for (int i = 60; i > 0; i--) {
      angle = 6 * i;
      endAngle = ((2 * pi) / 360) * angle + startAngle;
      double x = 0;
      double y = 0;

      x = center.width + cos(endAngle) * radius;
      y = center.height + sin(endAngle) * radius;
      // if (i % 5 == 0) {
      //   x = center.width + cos(endAngle) * (radius - 0);
      //   y = center.height + sin(endAngle) * (radius - 0);
      // } else {
      //   x = center.width + cos(endAngle) * radius;
      //   y = center.height + sin(endAngle) * radius;
      // }

      timeArray.add(
        Positioned(
          left: x,
          top: y,
          child: Transform.rotate(
            angle: endAngle,
            child: Container(
              width: i % 5 == 0 ? 8 : 6,
              height: 2,
              color: Colors.redAccent,
            ),
          ),
        ),
      );
    }

    return timeArray;
  }

  Widget _pointerSecond() {
    return SizedBox(
      width: 120,
      height: 10,
      child: ClipPath(
        clipper: SecondPath(),
        child: Container(
          decoration: const BoxDecoration(
            color: Colors.red,
          ),
        ),
      ),
    );
  }

  Widget _pointerMinute() {
    return SizedBox(
      width: 100,
      height: 15,
      child: ClipPath(
        clipper: MinutePath(),
        child: Container(
          decoration: const BoxDecoration(
            color: Colors.black54,
          ),
        ),
      ),
    );
  }

  Widget _pointerHour() {
    return SizedBox(
      width: 80,
      height: 20,
      child: ClipPath(
        clipper: MinutePath(),
        child: Container(
          decoration: const BoxDecoration(
            color: Colors.black,
          ),
        ),
      ),
    );
  }
}

class SecondPath extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    var path = Path();
    path.moveTo(size.width / 3, 0);
    path.lineTo(size.width, size.height / 2);
    path.lineTo(size.width / 3, size.height);
    path.lineTo(0, size.height / 2);
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) {
    return true;
  }
}

class MinutePath extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    var path = Path();
    path.moveTo(0, size.height / 3);
    path.lineTo(size.width, size.height / 5 * 2);
    path.lineTo(size.width, size.height / 5 * 3);
    path.lineTo(0, size.height / 3 * 2);
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) {
    return true;
  }
}

 

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

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

相关文章

YOLOv8独家改进:轻量化改进 | 高效移动应用的卷积加性自注意Vision Transformer

💡💡💡本文独家改进:轻量化改进之高效移动应用的卷积加性自注意Vision Transformer,构建了一个新颖且高效实现方式——卷积加性相似度函数,并提出了一种名为卷积加性标记混合器(CATM) 的简化方法来降低计算开销 💡💡💡性能比较:计算量参数量均有一定程度降低…

别盲目选择!2024年超级兔子与TOP3数据恢复,效率比拼全记录

在现在这个数字化的社会里&#xff0c;数据对我们来说太重要了。不管是家里的照片、工作文件&#xff0c;还是那些记录着美好时光的视频&#xff0c;要是弄丢了&#xff0c;肯定特别着急。不过别担心&#xff0c;今天咱们就来聊聊几款Windows系统上的数据恢复好帮手——超级兔子…

布局容器Grid、StackPanel、GroupBox、DockPanel、WrapPanel

Grid——网格布局&#xff0c;其中控件或容器需指定位置 StackPanel——堆叠面板&#xff0c;其中的控件水平布局、竖直布局 DockPanel——停靠面板&#xff0c;内部控件或容器可以放置在上、下、左、右 WrapPanel——可以看作是具有自动换行功能的StackPanel容器。窗体太小…

360杀毒恢复查杀的软件

360的查杀恢复区不太好找&#xff0c;特此记录&#xff1a; 主界面/管理中心面板/安全操作中心 安全操作中心/可恢复区&#xff1a;

UE5蓝图 抽卡出货概率

SSR概率0.1 SR概率0.2 R概率0.7 ps&#xff1a;数组内相加为1。且从小到大排序。

单片机相关面试问题精选

1. 基础概念类问题 什么是单片机&#xff1f;它有哪些主要应用&#xff1f; 答案要点&#xff1a;单片机是一种集成在单一芯片上的微型计算机&#xff0c;包含CPU、存储器、输入输出接口等&#xff0c;广泛应用于工业自动化、智能家居、汽车电子、医疗设备等领域。它能够实现复…

黑神话:悟空热背后的散热秘密:无压烧结银

黑神话&#xff1a;悟空热背后的散热秘密&#xff1a;无压烧结银 随着《黑神话:悟空》这款高画质、高性能要求的游戏在全球范围内的火爆&#xff0c;玩家们对于游戏设备的性能需求也达到了前所未有的高度。为了满足这种对极致游戏体验的追求&#xff0c;游戏主机和高端显卡等硬…

谷歌首页快捷方式变为一行的解决办法

也挺离谱的&#xff0c;今早上班刚打开谷歌浏览器&#xff0c;首页快捷方式就变成一排了&#xff0c;对于而且快捷方式还不能拖拽自定义排序&#xff0c;这使得我这位用习惯6年的双排老用户完全不能忍&#xff0c;打工人上班的怨气更重了。 经过几番周折中关于找到如下解决方案…

高级测试进阶 Centos7安装 Docker容器

前言 OS 安装环境要求 要安装 Docker Engine&#xff0c;需要 CentOS 7 的维护版本&#xff0c;不支持或未测试存档版本&#xff08;一句话&#xff1a;需要正常迭代版本的 Centos 7 系统&#xff0c;其他系统都不行&#xff09; 必须启用 centos-extras 存储库&#xff0c;…

Mysql 巧秒避开 varchar 类型的 max()、min() 函数的坑

比如&#xff0c;有一个这样的表&#xff0c; 里面存储的 数字 但是数据库表类型 是varchar 比如这个表的 nums &#xff1a; 样例数据&#xff1a; 如果我现在需要查询出这表里面&#xff0c;nums 最大的值 &#xff1a; 很多人可能不注意就会去使用 max &#xff08;&#…

08--kubernetes可视化界面与Daemonset

前言&#xff1a;前几章写的内容太多了&#xff0c;后面打算写k8s持久化篇幅也不小&#xff0c;这一章算作过度章节&#xff0c;内容简单一些&#xff0c;主要是K8S_web界面与Daemonset控制器。 1、Dashboard Dashboard是一个图形化界面&#xff0c;用于汇总和展示来自不同数…

酶荧光底物;Ac-ESEN-AMC;Ac-Glu-Ser-Glu-Asn-AMC;CAS:896420-43-2

【Ac-ESEN-AMC 简介】 Ac-Glu-Ser-Glu-Asn-AMC 通常用作酶的荧光底物&#xff0c;特别是作为溶酶体处理酶&#xff08;Vacuolar Processing Enzyme, VPE&#xff09;的选择性底物。在生物化学研究中&#xff0c;这类底物可以用于检测和定量特定酶的活性&#xff0c;因为当底物被…

最新盘点!适合制造业的工单管理系统有哪些?

本文带大家盘点好用的工单管理系统&#xff1a; 易维帮助台、金万维帮我吧、青鸟云报修、沃丰科技 ServiceGo、泛微工单管理系统、致远互联工单管理系统、腾讯云智服工单系统、Zendesk、Freshdesk。 工单管理系统就如同企业的高效调度员。它能把企业的各种任务和问题安排得有条…

【Material-UI】深入解析 Rating 组件中的 Radio Group 实现及其自定义技巧

文章目录 一、Rating 组件及其 Radio Group 实现概述1. Rating 组件介绍2. Rating 组件的 Radio Group 实现 二、Rating 组件的实现代码解析1. 自定义图标的使用2. 样式定制 三、Rating 组件中的 Radio Group 行为详解1. highlightSelectedOnly 属性的作用2. 图标容器的自定义3…

【python实现弹出文本输入框并获取输入的值】

在 Python 中可以使用easygui库来实现弹出文本输入框并获取输入的值。以下是具体的实现方法&#xff1a; 首先确保你安装了easygui库&#xff0c;如果没有安装&#xff0c;可以使用以下命令进行安装&#xff1a; pip install easygui以下是代码示例&#xff1a; import easy…

【html+css 绚丽Loading】 000023 八卦旋涡珠

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享htmlcss 绚丽Loading&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495…

【Python系列】Jinja2 模板引擎

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

其实Python的代码迁移并没有想象中复杂

声明&#xff1a;此篇为 ai123.cn 原创文章&#xff0c;转载请标明出处链接&#xff1a;https://ai123.cn/2257.html 提到Python&#xff0c;相信各位码农们都遇到过代码迁移的难题。我在处理版本兼容性问题时常常遇到Python 2与Python 3的不兼容&#xff0c;这给代码迁移带来了…

中国各企业避税程度相关数据(1998-2022年)

避税程度可以通过多种方式衡量&#xff0c;其中包括了名义所得税率与实际所得税率的差额&#xff08;RATE&#xff09;、名义所得税率与实际税率之差的五年平均值&#xff08;LRATE&#xff09;、会计与税收差异&#xff08;BTD&#xff09;以及扣除应计利润影响之后的会计与税…

树莓派+艺术品,有没有搞头?

由树莓派&#xff08;Raspberry Pi&#xff09;驱动的这一令人着迷的艺术品在国际上大受欢迎 Sisyphus Industries 公司的旗舰产品——具有家具和互动艺术品双重功能的沙盘。这个产品需要结构紧凑、价格低廉的控制硬件。Raspberry Pi 通过高度可靠的硬件和宝贵的庞大社区提供了…