flutter 手写时钟

news/2024/9/19 0:41:55/ 标签: flutter, 前端

前言:

之前看过别人写的 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> {@overridePath 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;}@overridebool 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);@overrideState<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;}@overridevoid initState() {// TODO: implement initStatesuper.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;});},);}@overridevoid dispose() {_timer?.cancel();// TODO: implement disposesuper.dispose();}@overrideWidget 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> {@overridePath 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;}@overridebool shouldReclip(CustomClipper<Path> oldClipper) {return true;}
}class MinutePath extends CustomClipper<Path> {@overridePath 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;}@overridebool shouldReclip(CustomClipper<Path> oldClipper) {return true;}
}

 


http://www.ppmy.cn/news/1519798.html

相关文章

尚品汇-MQ模块搭建测试、消息不丢失(重)(四十三)

目录&#xff1a; &#xff08;1&#xff09;消息不丢失 &#xff08;2&#xff09;消息确认 &#xff08;3&#xff09;消息确认业务封装 &#xff08;4&#xff09;封装发送端消息确认 &#xff08;5&#xff09;封装消息发送 &#xff08;6&#xff09;发送确认消息测试…

鸿蒙(API 12 Beta3版)【媒体资源使用指导】Media Library Kit媒体文件管理服务

应用可以通过photoAccessHelper的接口&#xff0c;对媒体资源&#xff08;图片、视频&#xff09;进行相关操作。 说明 在进行功能开发前&#xff0c;请开发者查阅[开发准备]&#xff0c;了解如何获取相册管理模块实例和如何申请相册管理模块功能开发相关权限。文档中使用到p…

基于深度学习的游客满意度分析与评论分析【情感分析、主题分析】

需要本项目的可以私信博主 目录 1 绪论 1.1 选题背景及研究意义 1.1.1 选题背景 1.1.2 研究意义 1.2 研究内容与方法 1.2.1 研究内容 1.2.2 研究方法 1.3 创新与不足 1.3.1创新点 1.3.2研究局限性 2 文献综述 2.1 相关概念界定 2.1.1 大数据分析 2.1.2 游客满意度 2.2 国内外研…

大数据系列之:查看Centos服务器用户可以创建的最大线程数、查看系统内核支持的最大线程数、查看系统支持的最大进程数、设置最大线程数限制、查看进程使用的线程数

大数据系列之:查看Centos服务器用户可以创建的最大线程数、查看系统内核支持的最大线程数、查看系统支持的最大进程数、设置最大线程数限制、查看进程使用的线程数 显示当前用户的资源限制查看用户可以创建的最大线程指定进程的资源限制查看系统内核支持的最大线程数查看系统支…

React 入门第八天:性能优化与开发者工具的使用

随着对React的逐步深入&#xff0c;我开始关注如何优化React应用的性能&#xff0c;特别是在复杂的组件树和频繁的状态更新中保持应用的高效性。这一天&#xff0c;我集中学习了React中的性能优化策略&#xff0c;并探索了如何使用React开发者工具来调试和优化应用。 1. 组件的…

续:当有数据时添加slave2

【示例】 另启一台虚拟机&#xff0c;作为mysql3. 新的虚拟机没有mysql软件包&#xff0c;如何才能快速部署&#xff1f;通过mysql1. mysql1&#xff1a; [rootmysql1 ~]# rsync -al /usr/local/mysql/ root172.25.254.166:/usr/local/mysql The authenticity of host 172.25…

Java算法之快速排序(Quick Sort)

快速排序&#xff1a;分而治之的高效排序算法 简介 快速排序是一种分而治之的排序算法&#xff0c;由C. A. R. Hoare在1960年提出。它通过选取一个元素作为"基准"&#xff08;pivot&#xff09;&#xff0c;然后重新排列数组&#xff0c;使得所有比基准值小的元素都…

【软考】【多媒体应用设计师】媒体与技术

1. 多媒体技术改变了传统循序式模式&#xff0c;用户可以借助超文本链接等方式&#xff0c;更自由灵活地访问所需的信息&#xff0c;体现了其&#xff08; &#xff09;的特点。 A.控制性 B.非线性 C.集成性 D.实时性 答案解析&#xff1a;本题考查信息多媒体非线性特点。多媒体…

安防监控/软硬一体/视频汇聚网关EasyCVR硬件启动崩溃是什么原因?

安防视频监控EasyCVR安防监控视频系统采用先进的网络传输技术&#xff0c;支持高清视频的接入和传输&#xff0c;能够满足大规模、高并发的远程监控需求。EasyCVR平台支持多种视频流的外部分发&#xff0c;如RTMP、RTSP、HTTP-FLV、WebSocket-FLV、HLS、WebRTC、WS-FMP4、HTTP-…

vue part 5

生命周期 <!DOCTYPE html> <html><head><meta charset"UTF-8" /><title>引出生命周期</title><!-- 引入Vue --><script type"text/javascript" src"https://cdn.jsdelivr.net/npm/vue/dist/vue.js&quo…

进程、线程的区别

进程&#xff08;Process&#xff09;和线程&#xff08;Thread&#xff09;是操作系统中的基本概念&#xff0c;它们在资源管理和任务执行方面有着本质的区别&#xff1a; 定义&#xff1a; 进程&#xff1a;进程是操作系统进行资源分配和调度的一个独立单位。每个进程都有自己…

ArcGIS Pro 3.1下载分享

在使用了很长一段时间ArcGIS Pro 3.0之后&#xff0c;终于迎来了ArcGIS Pro 3.1的更新&#xff0c;这里为你分享一下ArcGIS Pro 3.1的安装步骤。 软件介绍 ArcGIS Pro 3.1 是由Esri发布的地理信息系统 (GIS) 软件的较新版本&#xff0c;作为 ArcGIS 桌面应用程序家族中的核心…

【递归深搜之记忆化搜索算法】

1. 斐波那契数 解法一&#xff1a;递归 class Solution { public:int fib(int n) {return dfs(n);}int dfs(int n){if(n 0 || n 1)return n;return dfs(n - 1) dfs(n - 2);} }; 解法二&#xff1a;记忆化搜索 class Solution {int nums[31]; // 备忘录 public:int fib(int …

使用C++,仿照string类,实现myString

类由结构体演化而来&#xff0c;只需要将struct改成关键字class&#xff0c;就定义了一个类 C中类和结构体的区别&#xff1a;默认的权限不同&#xff0c;结构体中默认权限为public&#xff0c;类中默认权限为private 默认的继承方式不同&#xff0c;结构体的默认继承方式为p…

微型直线导轨高精度运行的工作原理

微型导轨是一种用于高精度定位和运动控制的传动装置&#xff0c;常用于微小化、高精密度化的机械设备中&#xff0c;如IC制造设备、半导体设备、高速移载的设备、精密测量、检测仪器、医疗设备、X-Y table&#xff0c;以及高速皮带驱动的设备等小型化设备。 微型导轨的构成相对…

单窗口IP代理设置指南:轻松搞定

在现代互联网生活中&#xff0c;IP代理已经成为了许多人日常上网的必备工具。单窗口IP代理是一种非常实用的代理方式&#xff0c;它允许你在同一个浏览器中为不同的窗口设置不同的IP地址&#xff0c;从而更好地保护隐私和实现多任务处理。今天&#xff0c;我们就来详细讲解一下…

在 macOS 上升级 Ruby 版本的几种方法

在 macOS 上升级 Ruby 版本通常有几种方法&#xff0c;以下是一些常用的方法&#xff1a; 使用系统自带的 Ruby: macOS 系统自带 Ruby&#xff0c;但通常不是最新版本。可以通过终端使用 softwareupdate 命令来更新系统自带的 Ruby。 使用 Homebrew: Homebrew 是 macOS 的包管…

字符串地指针表示方式

每日诗词&#xff1a; 人生自是有情痴&#xff0c;此恨不关风与月。 ——玉楼春尊前拟把归期说 【宋】欧阳修 目录 数组本身的值和数组储存的值一样吗 char[]和cahr*的区别 1. 类型 2. 内存分配 3. 使用方式 4. 字符串字面量 实例 变式 总结&#xff1a; 下期预告&a…

vue2+countup.js实现大屏数字滚动效果封装

很多大屏、官网或者展示类页面会用到数字跳动更新效果的需求&#xff0c;countup用起来就非常方便 一、官网 CountUp.js 二、效果图 三、安装countup与引入 npm install countup 进行安装依赖 import { CountUp } from countUp.js;//需要用到的页面引入&#xff0c;也可以…

生成式AI:创造性智能的新纪元

引言 随着人工智能技术的飞速发展&#xff0c;生成式AI&#xff08;Generative AI&#xff09;已经成为一个引人注目的领域。它不仅仅是模仿人类行为&#xff0c;而是通过学习大量的数据&#xff0c;创造出全新的内容&#xff0c;如文本、图像、音乐等。本文将探讨生成式AI的基…