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> {
  @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;
  }
}
相关推荐
PLM小助手7 分钟前
鼎捷新一代PLM 荣膺维科杯 “2023年度行业优秀产品奖”
java·大数据·前端·人工智能·分布式·低代码·微服务
F-1258 分钟前
关于 vue/cli 脚手架实现项目编译运行的源码解析
前端·javascript·vue.js
骨子里的偏爱11 分钟前
uniapp与webview直接进行传值
前端·chrome·uni-app
A_cot29 分钟前
HTML5全面知识点
前端·前端框架·html·html5
猛男敲代码30 分钟前
node强缓存
前端
杰哥技术分享1 小时前
fileinput pdf编辑初始化预览
java·前端·pdf
qiaocccccc1 小时前
项目中遇到的问题
前端·笔记
良_1231 小时前
keep-alive缓存不了iframe
前端
加勒比海涛2 小时前
Ajax 揭秘:异步 Web 交互的艺术
前端·ajax·交互