在闲暇冲浪的时候,无意间看到了这张设计图,眼睛一亮,感觉这个设计和创意非常酷,打算着手实现一下。关于设计图的作者没找到,如果有人知道的话,请告知我,我会添加设计引用的,欢迎来我的Github
源码Github
视频地址
实战抖音
分析设计,我们分两部分来实现:1.表盘部分 2.下部的开关部分。以下为了省略的逻辑代码,需要完整的代码的请移步Github
1.表盘部分
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();
渐变的背景.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();
}
绘制小圆点.png
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();
2. 开关部分
整体布局分析,开关部分的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),
),
)
],
),
);
}
整体效果图如下:
效果图.gif
网友评论