flutter天气_牛笔!自己用Flutter撸一个天气APP
這是一款簡約風格的 flutter 天氣項目,提供實時、多日、24 小時、臺風路徑以及生活指數等服務,支持定位、刪除、搜索等操作。
下圖為主頁效果:
開始
本身作為天氣 APP,自定義繪制自然少不了,首頁多樣的背景效果,炫酷的雨雪效果(https://juejin.im/post/6867489001809379335),展示當前空氣質量和體感的圓環效果,動態溫度折線圖和日出日落圖。
其實 pub.dev(https://pub.flutter-io.cn/packages?q=chart)上已經有不少 chart 插件,提供豐富的圖表類型,支持各種動畫和手勢。
但是如果是像本項目,使用場景并不需要手勢,且沒有復雜的動畫,只存在折線這種形態,完全可以自己實現。一方面可以鞏固和拓展 flutter 的繪制相關知識點,另一方面根據自己的實際需求,可以擁有更多的定制化功能。
先看一下最終效果,其中包括:
- 動態降雨折線圖
- 多日折線圖
- 24小時折線圖
- AQI圓弧
- 日出日落圖
繪制
接下來,會以上述效果作為切入點,由簡到難,由靜態到動態,逐步分析繪制前數據的準備和繪制時相關接口調用,最后,總結出折線圖繪制的通用思路,對后續有相關需求的小伙伴提供幫助。
AQI圓弧
先從最簡單圓弧圖開始,如上圖可看到的信息有:半透明的圓弧,純白色的圓弧,居中的 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.依次繪制中間 AQIValue 和 AQIDesc。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
日出日落貝塞爾曲線
上圖看起來像是圓弧,其實是使用二階貝塞爾曲線進行繪制。圖中涵蓋的信息并不多,其中包括左右日出日落時間、整體虛曲線、動態實曲線和當前時間。對于需要的數據除了日出日落時間,還需要根據 (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
多日折線圖
上下的文字區域繪制根據各自高度順延繪制即可,只要預留出中間折線的繪制區域即可。中間的折線區域又可以繼續平分成 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); }關鍵詞: 最大最小值
動態降雨折線圖
終于到了今天最難的角登場,只是對比前幾個比較難,在上述折線的基礎上加了折線入場動畫。話不多說咱們開始吧,上圖可拆成三部分,背景(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
總結
整體下來,無論是圓弧、曲線還是折線或者類似簡單的繪制都有章可循。
總結
以上是生活随笔為你收集整理的flutter天气_牛笔!自己用Flutter撸一个天气APP的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tlab java_浅析java中的TL
- 下一篇: java steam 去重_Java中对