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;
  }
}
相关推荐
passerby606115 分钟前
完成前端时间处理的另一块版图
前端·github·web components
掘了22 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅25 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
renke33641 小时前
Flutter for OpenHarmony:色彩捕手——基于HSL色轮与感知色差的交互式色觉训练系统
flutter
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax