flutter天气_牛笔!自己用Flutter撸一个天气APP

6e513432f284947614441cb63c558d07.png

这是一款简约风格的 flutter 天气项目,提供实时、多日、24 小时、台风路径以及生活指数等服务,支持定位、删除、搜索等操作。

下图为主页效果:

85ff99ac6bca6caab2496deb2bc6dd9a.png

开始

本身作为天气 APP,自定义绘制自然少不了,首页多样的背景效果,炫酷的雨雪效果(https://juejin.im/post/6867489001809379335),展示当前空气质量和体感的圆环效果,动态温度折线图和日出日落图。

其实 pub.dev(https://pub.flutter-io.cn/packages?q=chart)上已经有不少 chart 插件,提供丰富的图表类型,支持各种动画和手势。

但是如果是像本项目,使用场景并不需要手势,且没有复杂的动画,只存在折线这种形态,完全可以自己实现。一方面可以巩固和拓展 flutter 的绘制相关知识点,另一方面根据自己的实际需求,可以拥有更多的定制化功能。

先看一下最终效果,其中包括:

  • 动态降雨折线图

5ffea6c4877d3e85480ad893d63ff9f6.png
  • 多日折线图

8249fbfce2a84a466de64a68ba4a83a0.png
  • 24小时折线图

0920cce9dd0aea0d35455e800ee19bf9.png
  • AQI圆弧

3bb71277323b738c5d686735ff4ca46c.png
  • 日出日落图

14b92a765323209a88ad7e4b1f8dbf5e.png

绘制

接下来,会以上述效果作为切入点,由简到难,由静态到动态,逐步分析绘制前数据的准备和绘制时相关接口调用,最后,总结出折线图绘制的通用思路,对后续有相关需求的小伙伴提供帮助。

AQI圆弧

3bb71277323b738c5d686735ff4ca46c.png

先从最简单圆弧图开始,如上图可看到的信息有:半透明的圆弧,纯白色的圆弧,居中的 AQI 值以及其底部的文字描述。对于此图而言,只需要知道 ratio: 白色圆弧占比、AQIValue 和 AQIDesc。这个简单直接先上代码再分析。

@overridevoid paint(Canvas canvas, Size size) {weatherPrint("AqiChartPainter size:$size");var radius = size.height / 2 - 10;var centerX = size.width / 2;var centerY = size.height / 2;var centerOffset = Offset(centerX, centerY);// 绘制半透明圆弧_path.reset();_path.addArc(Rect.fromCircle(center: centerOffset, radius: radius),pi * 0.7, pi * 1.6);_paint.style = PaintingStyle.stroke;_paint.strokeWidth = 4;_paint.strokeCap = StrokeCap.round;_paint.color = Colors.white38;canvas.drawPath(_path, _paint);// 绘制纯白色圆弧_path.reset();_path.addArc(Rect.fromCircle(center: centerOffset, radius: radius),pi * 0.7, pi * 1.6 * ratio);_paint.color = Colors.white;canvas.drawPath(_path, _paint);// 绘制 AQIValuevar valuePara = UiUtils.getParagraph(value, 30);canvas.drawParagraph(valuePara,Offset(centerOffset.dx - valuePara.width / 2,centerOffset.dy - valuePara.height / 2));// 绘制 AQIDescvar descPara = UiUtils.getParagraph("$desc", 15);canvas.drawParagraph(descPara,Offset(centerOffset.dx - valuePara.width / 2,centerOffset.dy + valuePara.height / 2));}

1. 先绘制半透明圆弧,确认中心点坐标和半径,通过 _path.addArc(Rect oval, double startAngle, double sweepAngle) 方法进行绘制。oval: 圆弧所在矩形,startAngle: 起始角度(以钟表为例,0为3点方向),sweepAngle: 划过角度(默认方向顺时针)。

2. 在半透明圆弧基础上,根据 ratio (currentAqiValue / totalAqiValue) 绘制纯白色圆弧

3.依次绘制中间 AQIValueAQIDesc。Flutter 绘制文本跟 Android 比起来略微有点麻烦,通过构造 ui.Paragraph 对象,然后调用 canvas.drawParagraph(Paragraph paragraph, Offset offset) 方法进行绘制。一般通过封装好的静态初始化方法构建ui.Paragraph 对象:

static ui.Paragraph getParagraph(String text, double textSize,{Color color = Colors.white, double itemWidth = 100}) {var pb = ui.ParagraphBuilder(ui.ParagraphStyle(textAlign: TextAlign.center, //居中fontSize: textSize, //大小));pb.addText(text);pb.pushStyle(ui.TextStyle(color: color));var paragraph = pb.build()..layout(ui.ParagraphConstraints(width: itemWidth));return paragraph;}

关键词: addArc、Paragraph、 drawParagraph

日出日落贝塞尔曲线

ac87fa665b07da8476cef19288d0b371.png

上图看起来像是圆弧,其实是使用二阶贝塞尔曲线进行绘制。图中涵盖的信息并不多,其中包括左右日出日落时间、整体虚曲线、动态实曲线和当前时间。对于需要的数据除了日出日落时间,还需要根据 (nowTime - sunriseTime)/(sunsetTime - sunriseTime) 获取占比 ratio。继续分解步骤:**1. **绘制 虚曲线,首先确认起点和终点,通过 _path.quadraticBezierTo(double x1, double y1, double x2, double y2) 绘制贝塞尔曲线,参数需要传入 控制点 坐标和 终点 坐标。很遗憾 Flutter 没有提供虚线的接口,借用 path_drawing 插件中的 dashPath(Path source, {@required CircularIntervalList<double> dashArray,DashOffset dashOffset,}) 方法进行虚线的绘制。

var height = size.height;
var width = size.width;
double startX = marginLeftRight;
double startY = height - marginBottom;
double endX = width - marginLeftRight;
double endY = startY;
_path.reset();
_path.moveTo(startX, startY);
_path.quadraticBezierTo(width / 2, marginTop, endX, endY);
_paint.color = Colors.white;
_paint.style = PaintingStyle.stroke;
_paint.strokeWidth = 1.5;
canvas.drawPath(dashPath(_path, dashArray: CircularIntervalList<double>([10, 5])),_paint);

**2. **绘制 实虚线,这里遇到一个问题,已知比例 ratio,在虚曲线上绘制实曲线(保证重叠),不同于直线或者弧线,通过控制 xy 或者 sweepAngle 轻松实现。对二阶贝塞尔曲线稍有了解的可以知道,其主要由起始点和控制点组成,这三个值稍有变化,都很难做到重叠,所以得另辟蹊径。Android 中有 PathMeasure 可以对 Path 进行分段,然后根据需要绘制的段数进行控制。同样,Flutter 也有对应的 API:

var metrics = _path.computeMetrics();
var pm = metrics.elementAt(0);
Offset sunOffset = pm.getTangentForOffset(pm.length * ratio).position;
canvas.save();
canvas.clipRect(Rect.fromLTWH(0, 0, sunOffset.dx, height));
canvas.drawPath(_path, _paint);
canvas.restore();

通过 getTangentForOffset 得到 ratio 下在曲线上的 x,y 坐标点,然后 _path.clipRect() 对虚曲线裁剪最终得到实曲线。3. 绘制小太阳和当前时间,知道曲线上的 x,y 坐标,这就好办了

_paint.style = PaintingStyle.fill;
_paint.color = Colors.yellow;
canvas.drawCircle(sunOffset, 6, _paint);var now = DateTime.now();
String nowTimeStr = "${now.hour}:${now.minute}";
var nowTimePara = UiUtils.getParagraph(nowTimeStr, 14);
canvas.drawParagraph(nowTimePara,Offset(sunOffset.dx - nowTimePara.width / 2, sunOffset.dy + 10));

关键词: quadraticBezierTo、dashPath、computeMetrics、getTangentForOffset、clipRect、drawCircle

多日折线图

1c56f19ac971456ace1d8d087c4b70f9.png

上下的文字区域绘制根据各自高度顺延绘制即可,只要预留出中间折线的绘制区域即可。中间的折线区域又可以继续平分成 top 和 bottom 两个折线,各自绘制各自的,互不干扰。折线图的绘制思路分为三步:找出最大最小值、计算单位温度的 y 值和遍历绘制1. 遍历找出 top 和 bottom 的最大最小值

void setMinMax() {_data.forEach((element) {if (element.dayTemp > topMaxTemp) {topMaxTemp = element.dayTemp;}if (element.dayTemp < topMinTemp) {topMinTemp = element.dayTemp;}if (element.nightTemp > bottomMaxTemp) {bottomMaxTemp = element.nightTemp;}if (element.nightTemp < bottomMinTemp) {bottomMinTemp = element.nightTemp;}});
}

2. 根据温度计算x,y值,目前已知折线的高度 itemHeight, 具体温度 temp,起点 topLineStartY,最高最低温度已经实际温度,即可算出温度对应的 y 坐标值,x坐标值

getTopLineY(int temp) {if (temp == topMaxTemp) {return topLineStartY;}return topLineStartY +(topMaxTemp - temp) / (topMaxTemp - topMinTemp) * lineHeight;
}
x = startX + index*itemWidth;

**3. **开始绘制,x,y 都知道了,直线、原点以及文字都可以进行遍历绘制了

_paint.color = Colors.white;
var topOffset = Offset(startX, getTopLineY(element.dayTemp));
var bottomOffset = Offset(startX, getBottomLineY(element.dayTemp));
_paint.style = PaintingStyle.fill;
// 绘制折线上的圆点
canvas.drawCircle(topOffset, 3, _paint);
canvas.drawCircle(bottomOffset, 3, _paint);// 绘制圆点上下的温度值
var topTempPara = UiUtils.getParagraph("${element.dayTemp}°", mainTextSize, itemWidth: itemWith);
canvas.drawParagraph(topTempPara, Offset(topOffset.dx - topTempPara.width / 2, topOffset.dy - topTempPara.height - 5));
var bottomTempPara = UiUtils.getParagraph("${element.dayTemp}°", mainTextSize, itemWidth: itemWith);
canvas.drawParagraph(bottomTempPara, Offset(bottomOffset.dx - bottomTempPara.width / 2, bottomOffset.dy + 5));// 绘制折线
if (index == 0) {_topPath.moveTo(topOffset.dx, topOffset.dy);_bottomPath.moveTo(bottomOffset.dx, bottomOffset.dy);
} else {_topPath.lineTo(topOffset.dx, topOffset.dy);_bottomPath.lineTo(bottomOffset.dx, bottomOffset.dy);
}
startX += itemWith;
});
_paint.strokeWidth = 2;
_paint.style = PaintingStyle.stroke;
canvas.drawPath(_topPath, _paint);
canvas.drawPath(_bottomPath, _paint);
}

关键词: 最大最小值

动态降雨折线图

5ffea6c4877d3e85480ad893d63ff9f6.png

终于到了今天最难的角登场,只是对比前几个比较难,在上述折线的基础上加了折线入场动画。话不多说咱们开始吧,上图可拆成三部分,背景(y轴,xy轴描述)、渐变折线和动画

  • 背景

x 轴被二等分,y 轴被三等分,计算出 xItemWidth 和 yItemHeight,然后绘制线和文字

void drawBg(Canvas canvas, Size size) {// 绘制背景 linedouble itemHeight = (size.height - _marginBottom) / 3;double bgLineWidth = size.width - _marginLeft - _marginRight;_paint.style = PaintingStyle.stroke;_paint.strokeWidth = 1;_paint.color = Colors.white.withAlpha(100);for (int i = 0; i < 4; i++) {var startOffset = Offset(_marginLeft, itemHeight * i);var endOffset = Offset(_marginLeft + bgLineWidth, itemHeight * i);canvas.drawLine(startOffset, endOffset, _paint);}// 绘制底部文字var hourY = size.height - _marginBottom + _timeMarginTop;var nowPara = UiUtils.getParagraph("现在", _textSize, itemWidth: bgLineWidth / 3);canvas.drawParagraph(nowPara, Offset(_marginLeft - nowPara.width / 2, hourY));var onePara = UiUtils.getParagraph("1小时后", _textSize, itemWidth: bgLineWidth / 3);canvas.drawParagraph(onePara, Offset(_marginLeft + bgLineWidth / 2 - onePara.width / 2, hourY));var twoPara = UiUtils.getParagraph("2小时后", _textSize, itemWidth: bgLineWidth / 3);canvas.drawParagraph(twoPara, Offset(_marginLeft + bgLineWidth - twoPara.width / 2, hourY));// 绘制左侧文字var bigPara = UiUtils.getParagraph("大", _textSize);canvas.drawParagraph(bigPara, Offset(_marginLeft / 2 - bigPara.width / 2, 0));var middlePara = UiUtils.getParagraph("中", _textSize);canvas.drawParagraph(middlePara, Offset(_marginLeft / 2 - middlePara.width / 2, itemHeight));var smallPara = UiUtils.getParagraph("小", _textSize);canvas.drawParagraph(smallPara, Offset(_marginLeft / 2 - smallPara.width / 2, itemHeight * 2));
}
  • 渐变折线

1. 绘制折线,最大值不用计算已经知道 yMax = 1.0,xMax = 120,可以计算出点的 x,y 坐标值,然后进行遍历绘制

double width = size.width - _marginLeft - _marginRight;
double height =  size.height - _marginBottom;
double startX = _marginLeft;
double itemWidth = width / 120;
double itemHeight = height / 100;
_linePath.reset();
for (int i = 0; i < _data.length; i++) {double y = height - _data[i] * 100 * itemHeight * _ratio;double x = startX + i * itemWidth;if (i == 0) {_linePath.moveTo(x, y);} else {_linePath.lineTo(x, y);}
}
_linePaint.style = PaintingStyle.stroke;
_linePaint.strokeWidth = 1;
_linePaint.color = Colors.white;
canvas.drawPath(_linePath, _linePaint);
_linePath.lineTo(width + startX, height);
_linePath.lineTo(startX, height);
_linePath.close();

2. 渐变效果,复用折线 path,通过 ui.Gradient.linear 创建渐变区域,然后设置到 _linePaint.shader 上

var gradient = ui.Gradient.linear(Offset(0, 0),Offset(0, height),<Color>[const Color(0xFFffffff),const Color(0x00FFFFFF)],
);
_linePaint.style = PaintingStyle.fill;
_linePaint.shader = gradient;
canvas.drawPath(_linePath, _linePaint);
  • 入场动画

渐变折线#1 中对 y 的计算 double y = height - _data[i] * 100 * itemHeight * _ratio; 中提到了 _ratio,这个就是控制动画效果关键变量,区间 [0,1],0为y=0.0 的直线,1为实际的折线图效果。而这个 _ratio 有动画进行控制:

_controller =AnimationController(duration: Duration(milliseconds: 250), vsync: this);
CurvedAnimation(parent: _controller, curve: Curves.linear);
_controller.addListener(() {setState(() {_ratio = _controller.value;});
});

最终的动态折线效果即可完成。关键词:drawLine、ui.Gradient.linear、AnimationController

总结

整体下来,无论是圆弧、曲线还是折线或者类似简单的绘制都有章可循。

  1. 对 待实现效果进行分析,找出关键信息进行分层分步,找出静态数据和动态数据,也就是常量和变量。
  2. 计算好基础数据,比如整体宽高,单位宽高,起始值,最大最小值
  3. 有了数据支撑,根据效果调用对应的绘制 API,设置 paint 的相关属性,完成绘制
  4. 如果有动画,以控制变量作为切入口,动画本身只关注变量值的改变,而不用考虑变量对绘制的影响
原文:下位子 的个人主页

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/501006.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电脑远程凭证不工作:解决

电脑 远程桌面连接你的凭据不工作解决方法 方法/步骤 第一步我们首先需要知道远程桌面连接你的凭据不工作原因是&#xff0c;远程的电脑拒绝了访问&#xff0c;需要设置在远程的电脑上设置安全选项&#xff0c;按winR键&#xff0c;打开运行&#xff0c;输入“gpedit.msc”&a…

python汉诺塔递归算法_Python文摘:汉诺塔问题与递归算法

历史传说&#xff1a; 在世界中心贝拿勒斯&#xff08;在印度北部&#xff09;的圣庙里&#xff0c;一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候&#xff0c;在其中一根针上从下到上地穿好了由大到小的64片金片&#xff0c;这就是所谓的汉诺塔。不论白天黑夜…

转-递归教学

作者&#xff1a;帅地 链接&#xff1a;https://www.zhihu.com/question/31412436/answer/683820765 来源&#xff1a;知乎 著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 递归专题连续刷题半年&#xff0c;从小白到学会了套路&#xff…

android游戏编程之从零开始_纯C语言程序员写的编程新手入门基础小游戏之最炫酷推箱子...

很多编程爱好者都编写过推箱子游戏编程吧&#xff0c;最近有好些朋友看见我以前的推箱子程序后&#xff0c;问我是怎么做的。我一直想把这个程序的整个过程写一份详细的东西&#xff0c;与各位编程爱好者分享&#xff0c;一直没空。正好现在放假了&#xff0c;而且离回家还有几…

c++ h cpp文件如何关联_C++核心准则SF.5: .cpp文件必须包含定义它接口的.h文件

SF.5: A .cpp file must include the .h file(s) that defines its interfaceSF.5: .cpp文件必须包含定义它接口的.h文件Reason(原因)This enables the compiler to do an early consistency check.这样可以让编译器尽早进行一致性检查。Example, bad(反面示例)// foo.h:void f…

JAVA进阶教学之(IO流)

目录 1、什么是IO流 2、流的分类 3、流的四大家族首领 4、java.io.*包下需要掌握的16个流 5、FileInputStream的实用方法 6、FileOutputStream的方法 7、文件复制/拷贝 8、FileReader的使用 9、FileWriter的使用 10、复制普通文本文件 11、BufferedReader带有缓冲区…

devtools安装_R语言如何批量安装软件包

1. 为什么要批量安装R语言包当你在新的环境下&#xff0c; 安装R语言时&#xff0c;你需要安装很多包&#xff0c;比如tidyverse&#xff0c;比如data.table&#xff0c;这里你可以写一个函数&#xff0c;将所有需要的包写进去&#xff0c;然后进行批量安装2. 程序如下&#xf…

JAVA进阶教学之(序列化和反序列化)

目录 1、序列化Serialize和反序列化的概念 2、序列化和反序列化的代码演示&#xff1a; 3、序列化多个对象&#xff08;序列化集合&#xff09; 4、transient关键字将部分属性不参与序列化 1、序列化Serialize和反序列化的概念 在内存和硬盘的数据交互过程中&#xff0c;将…

java如何实现e的次方_Java开发如何更改MySQL数据库datadir目录之MySQL数据库索引实现...

引言MySQL是一个关系型数据库管理系统&#xff0c;由瑞典MySQL AB 公司开发&#xff0c;目前属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一&#xff0c;在 WEB 应用方面&#xff0c;MySQL是最好的 RDBMS (Relational Database Management System&#xff0c…

pytorch 训练过程acc_Pytorch之Softmax多分类任务

在上一篇文章中&#xff0c;笔者介绍了什么是Softmax回归及其原理。因此在接下来的这篇文章中&#xff0c;我们就来开始动手实现一下Softmax回归&#xff0c;并且最后要完成利用Softmax模型对Fashion MINIST进行分类的任务。在开始实现Softmax之前&#xff0c;我们先来了解一下…

进程调度实验_Linux应用编程之进程的PID与PPID

关注、星标公众号&#xff0c;直达精彩内容ID&#xff1a;嵌入式情报局作者&#xff1a;情报小哥1进程PID首先介绍PID的相关知识&#xff0c;为后面介绍fork函数进行铺垫。01PID与PPID PID不是控制理论的PID算法&#xff0c;而是Prcess ID的简写。进程PID是当操作系统运行进程时…

操作Windows文件夹时,弹出文件夹正在使用,操作无法完成【解决】

在windows系统上&#xff0c;有时候在删除系统文件或文件夹时出现弹框&#xff0c;提示操作无法完成。这种情况的出现是因为你要删除的文件或文件夹被打开&#xff0c;或者被系统占用。遇到这种情况要怎么处理呢&#xff0c;本文介绍下具体的操作方法来帮助你解决这个问题。 方…

邀请合作如何表达_适时表达想法 才有利于彼此的合作

丹尼跟珍妮合作主持一个podcast节目&#xff0c;两人对这个节目兴致勃勃&#xff0c;并花很多时间投入&#xff0c;珍妮想邀请自己身边朋友一起参加&#xff0c;认为特别来宾可以增加节目的丰富度&#xff1b;丹尼却觉得现在节目才刚开始起步&#xff0c;要建立好两人的节目定位…

idea代码可以编译但是爆红_推荐一款 IDEA 生成代码神器,写代码再也不用加班了...

作者&#xff1a;HeloWxl链接&#xff1a;https://www.jianshu.com/p/e4192d7c6844Easycode是idea的一个插件&#xff0c;可以直接对数据的表生成entity,controller,service,dao,mapper,无需任何编码&#xff0c;简单而强大。1、安装(EasyCode)我这里的话是已经那装好了。建议大…

html跑马灯_用Excel居然能做“跑马灯”,而且还这么简单!

我的目标&#xff1a;让中国的大学生走出校门的那一刻就已经具备这些office技能&#xff0c;让职场人士能高效使用office为其服务。支持我&#xff0c;也为自己加油&#xff01;你没看错&#xff0c;上面这个就是用Excel做出来的&#xff0c;不过要用到窗体和控件。步骤如下&am…

c语言双链表排序交换节点_图解:单链表翻转的三种方式!

当我们在聊到链表反转的时候&#xff0c;一定说的都是单链表&#xff0c;双链表本身就具有前驱指针 Prev 和后续指针 next&#xff0c;无需进行翻转。单链表反转&#xff0c;反转后的效果如下&#xff1a;看起来很简单&#xff0c;只需要将单链表所有结点的 next 指向&#xff…

wsdl文档中的soap:address的生成规则_BAT大牛都在使用的数据库文档生成插件,不来看一下?...

一、概述在企业级开发中、我们经常会有编写数据库表结构文档的时间付出&#xff0c;从业以来&#xff0c;待过几家企业&#xff0c;关于数据库表结构文档状态&#xff1a;要么没有、要么有、但都是手写、后期运维开发&#xff0c;需要手动进行维护到文档中&#xff0c;很是繁琐…

修订模式怎么彻底关闭_电脑玩游戏卡顿怎么办?

电脑玩游戏卡怎么办&#xff1f;在玩游戏时电脑卡真的是会气死人的&#xff0c;特别是在打团的时候卡了&#xff0c;想砸电脑有木有&#xff1f;那么电脑玩游戏卡怎么办呢&#xff1f;给大家介绍几个方法&#xff0c;可以尝试改善卡顿。软件方面&#xff1a;1、 开启电源性能模…

datepicker不能选择是为什么_为什么客厅不好看?休闲椅选错了

为什么客厅不好看&#xff1f;休闲椅选错了很多装修完毕的小伙伴们经常有这样一个疑问: 为什么我家的客厅看上去这么凌乱&#xff0c;一点都没有想象中井然有致&#xff1f;这其中的潜在原因有很多&#xff0c;比如沙发墙的装饰设计有误&#xff0c;比如各类家具的款式搭配不对…

如何打开屏幕坏的手机_每天打开手机屏幕20次?打开10次以上的朋友进~

现代社会最很普遍的现象就是不管有没有事&#xff0c;不断地打开手机屏幕看时间或者刷各种信息和段子。 一块小小的屏幕却有着巨大的魅力。明明没有任何事情要干&#xff0c;却还是忍不住诱惑(cant resist temptation [tɛmpˈteʃən])想要打开屏幕&#xff0c;仿佛潘多拉的盒…