前言:
之前看过别人写的 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;
}
}