Flutter仿Dribbble的扫描闹钟酷炫效果 | 自绘实现!

b43c5a3003c3fc0cef6071d7d31565d5.jpeg

码农钉某人 | 作者

https://juejin.cn/post/6959093549304381454 | 原文

Hi,大家好,这里是承香墨影!

在闲暇冲浪的时候,无意间看到了这张设计图,眼睛一亮,感觉这个设计和创意非常酷,打算着手实现一下。关于设计图的作者没找到,如果有人知道的话,请告知我,我会添加设计引用的,欢迎来我的 Github。

分析设计,我们分两部分来实现:

  1. 表盘部分;

  2. 下部的开关部分;

以下为了省略的逻辑代码,需要完整的代码的请移步 Github。

  • Github:
    https://github.com/DingMouRen/cool_flutter_ui

一、表盘部分

1.1 观察表盘的样式,把需要做的任务划分

  • 渐变的背景,用作秒针;

  • 绘制白色的小圆点;

  • 绘制带阴影的黄色大圆点,用作时针;

  • 绘制时间文字;

  • 开启定时器,让时钟动起来;

表盘部分很多都是需要我们自己绘制的,这里我们可以使用 CustomPaint 来实现所有的绘制。

CustomPaint 提供了自定义 Widget 的能力,它会暴露一个 canvas,可以通过这个 canvas 来绘制 widget,有没有很熟悉,跟原生 android 的 canvas 是不是很相似。

我们这里将表盘作为背景来绘制,也就是 painter 属性,自定义 CustomPainter,重写 paint(Canvas canvas,Size size)shouldRepaint(covariant CustomPainter oldDelegate) 函数,来完成我们所有的绘制操作。

好的,让我们愉快的开始吧 o( ̄▽ ̄) ブ。

1.2 渐变的背景,用作秒针

从图中我们可以知道,渐变色是扫描渐变,并布满屏幕,同时位置在整个屏幕的上半部分。

首先创建扫描渐变对象,这个扫描渐变可以创建出一个着色器,然后我们将这个着色器附着在一个画笔上,通过画布去绘制。

注意画布绘制时 canvas.save()canvas.restore() 在对应时机的调用。要让秒针动起来,需要开启一个 Timer 定时任务,每一秒刷新一下视图。

var circle = Rect.fromCircle(center: Offset(0, 0), radius: _screenHeight);
//扫面渐变
var sweepGradient = SweepGradient(
  colors: [
    _startColor,
    _endColor
  ],
);
//画笔对象
Paint  _paintGradient = Paint()
  ..isAntiAlias = true
  ..shader = sweepGradient.createShader(circle)
  ..style = PaintingStyle.fill;

//获取当前的时间
DateTime dateTime = DateTime.now();
var hour = dateTime.hour;
var minute = dateTime.minute;
var second = dateTime.second;
//画布位移
canvas.translate(_screenWidth / 2, _screenHeight / 100 * 35);
//绘制渐变背景
canvas.save();
//每秒旋转对应角度,模拟秒针移动
canvas.rotate(_getRotate(second));
canvas.drawCircle(Offset(0, 0), _screenHeight, _paintGradient);
canvas.restore();
87a7088194f1c742b7f230d34ff94fbb.gif

1.3 绘制白色的小圆点

这里需要绘制 24 个白色小圆点,平均分 360 度, 通过画布的旋转来绘制不同角度的白色小圆点。

for (double i = 0; i < _numPoint; i++) {
  canvas.save();
  //
  double deg = 360 / _numPoint * i;
  canvas.rotate(deg / 180 * pi);
    _paintDial.color = Colors.white;
   //绘制白色小圆点
   canvas.drawCircle(Offset(_radius, 0), 3, _paintDial);
   canvas.restore();

  ......
  canvas.restore();
}
bd98b2f483b09c5960f0c8a4d68abfa4.jpeg

1.4 绘制带阴影的黄色大圆点,用作时针

黄色的大圆点作为时针来处理,因为时针和画布的角度不是吻合的,需要换算,另外我们为大圆点添加阴影。

for (double i = 0; i < _numPoint; i++) {
  canvas.save();
  double deg = 360 / _numPoint * i;
  canvas.rotate(deg / 180 * pi);
  _paintDial.color = Colors.white;
  canvas.drawCircle(Offset(_radius, 0), 3, _paintDial);
  //isShowBigCircle(hour, i)是判断当前圆点是不是当前的时针位置
  if (isShowBigCircle(hour, i)) {
    //绘制阴影
    Path path = Path()
      ..addArc(Rect.fromCircle(center: Offset(_radius, 0), radius: 8), 0,
          pi * 2);
    canvas.drawShadow(path, Colors.yellow, 4, true);
    //绘制小时的圆点
    _paintDial.color = Colors.yellow;
    canvas.drawCircle(Offset(_radius, 0), 8, _paintDial);
  } else {
    _paintDial.color = Colors.white;
    canvas.drawCircle(Offset(_radius, 0), 3, _paintDial);
  }
  canvas.restore();
  ......
}

1.5 绘制时间文字

画布绘制文字调用 canvas.drawParagraph(Paragraph Offset) 函数,Paragraph 对象通过 ParagraphBuilder 来创建,字体样式可以通过 ParagraphBuilder 来设置。

//设置文字样式
_timeParagraphBuilder = ParagraphBuilder(ParagraphStyle(
    textAlign: TextAlign.center,
    fontSize: 70,
    maxLines: 1,
    fontWeight: FontWeight.bold));
//获取当前的时间
DateTime dateTime = DateTime.now();
var hour = dateTime.hour;
var minute = dateTime.minute;
var second = dateTime.second;
//绘制文字
canvas.save();
_timeParagraphBuilder.addText(_getTimeStr(hour, minute));
Paragraph paragraph = _timeParagraphBuilder.build();
paragraph.layout(ParagraphConstraints(width: 230));
canvas.drawParagraph(paragraph, Offset(-115,-42));
canvas.restore();

二、开关部分

整体布局分析,开关部分的 UI 相对于整个屏幕来讲位于底部,使用 Stack 和 Align 就可以实现这种布局样式。

//整体布局
@override
Widget build(BuildContext context) {
  return Scaffold(
      body: Stack(
    children: [
      CustomPaint(painter: DialPlate(context,Color.fromARGB(255, 70, 0, 144),Color.fromARGB(255, 121, 83, 254))),
      _getAlarms(),
    ],
  ));
}
//下部视图
_getAlarms() {
  return Align(
    alignment: Alignment.bottomLeft,
    child: Container(
      margin: EdgeInsets.only(left: 16, right: 16),
      height: 200,
      width: double.infinity,
      child: Column(
        children: [
          _getRow1(),
          _getRow2(),
          _getRow3(),
        ],
      ),
    ),
  );
}
//_getRow1()、_getRow2()、_getRow3()类似
_getRow1() {

  return Container(
    alignment: Alignment.centerLeft,
    width: double.infinity,
    height: 50,
    child: Row(
      children: [
        Text(
          '06:45',
          style: TextStyle(
            fontSize: 25,
            color: _firstSwitch == true ? _colorOn : _colorOff),
        ),
        Padding(
          padding: EdgeInsets.only(left: 18),
          child: Text(
            'Wake up',
            style: TextStyle(
              fontSize: 18,
              color: _firstSwitch == true ? _colorOn : _colorOff),
          ),
        ),
        Expanded(child: SizedBox()),
        Container(
          width: 90,
          height: 10,
          child: Switch(
            value: _firstSwitch,
            onChanged: (onChanged) {
              setState(() {_firstSwitch = onChanged;});
            },
            activeColor: _switchActiveColor,
            activeTrackColor: Colors.black.withAlpha(100),
            inactiveThumbColor: _switchInActiveColor,
            inactiveTrackColor: Colors.black.withAlpha(20),
          ),
        )
      ],
    ),
  );
}
整体效果图如下:
dda6d144fde98b6fb96eade767dd566f.gif

-- End --

本文对你有帮助吗?留言、转发、点好看是最大的支持,谢谢!

推荐阅读:

点亮技能树 - 从害怕到玩转 Android 代码混淆!

Android 视图系统的设计与实现 | 通俗易懂

面试官:Activity finish 后,会立即回调 onDestory() 吗?若不会,最长延迟多久?

26219af0570b9911c82a7e2e3bf37994.jpeg
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值