本文介绍Flutter_Weather天气模块实现。效果图如下:

首页最外层布局实现
首页包含一个顶部的城市名称展示栏和一个pageview。因此可以使用一个Column
竖直的列进行包裹。
return Container(
child: Column(
children: <Widget>[
//头
buildBar(context),
//pageview
Expanded(child: _buildPageView(),
)
],
),
);
使用Expanded
填充剩余空间,类似Android权重属性。
PageView实现

_buildPageView()
根据 loadState加载状态不同返回3个widget。加载数据时返回一个自定义的ProgressView,加载失败时返回一个失败的Widget,只有当数据加载成功时,才返回PageView。
PageView属性:
- scrollDirection :滚动方向。 Axis.horizontal 横向 vertical竖向
- controller : PageController 控制pageview滚动
- pageSnapping : 默认为true。设置false后失去pageview的特性
顶部标题栏实现


//跳转页面并接收返回值
Future future = Application.router.navigateTo(context, Routes.cityPage);
//接收回调信息
future.then((value){
if(value != null){
}
});
数据加载
1、加载assets中json数据
因为数据调用的次数是有限制的,所以在调试的时候只能加载本地的数据了╮(╯▽╰)╭
//从assets中加载天气信息
loadWeatherAssets() async {
Future<String> future = DefaultAssetBundle.of(context).loadString("assets/json/weather.json");
future.then((value){
setState(() {
weatherJson = value;
});
});
}
flutter推荐我们使用DefaultAssetBundle
进行本地数据加载。
加载网络数据
loadWeatherData() async {
final response = await http.get(Api.WEATHER_QUERY + city);
setState(() {
weatherJson = response.body;
});
}
你没看错,就一行代码就搞定了数据加载。当然要使用await
来等待加载完成,因为有等待,所以加载的方法要async
在异步中进行。
Json解析
加载完数据以后进行json解析
导包 import 'dart:convert';
if(weatherJson.isNotEmpty){
WeatherBean weatherBean = WeatherBean.fromJson(json.decode(weatherJson));
if(weatherBean.succeed()){
loadState = 1;
weatherResult = weatherBean.result;
}else{
loadState = 0;
}
}
json.decode()
返回的是一个dynamic
任意类型。因此需要我们在手动解析。
解析对象
WeatherBean中实现如下:

WeatherBean.fromJson(Map<String,dynamic> json)
手动解析。
如果解析的key是一个对象,例如上面的WeatherResult
对象。则需要调用WeatherResult对象的fromJson。
为了保险起见,解析WeatherResult对象的时候加一个非空判断。
解析数组
我们再来看一下WeatherResult中又是啥。(有点多,截屏截不全了╮(╯▽╰)╭,就拷贝吧)
class WeatherResult{
final String city; //城市
final String citycode; //城市code (int)
...(省略一些)
final Aqi aqi;
final List<WeatherIndex> indexs; //生活指数
final List<WeatherDaily> dailys; //一周天气
final List<WeatherHourly> hours; //24小时天气
WeatherResult({this.city,this.citycode,this.date,this.weather,this.temp,this.temphigh,this.templow,this.img,this.humidity,
this.pressure,this.windspeed,this.winddirect,this.windpower,this.updatetime,this.week,this.aqi,this.indexs,this.dailys,this.hours});
factory WeatherResult.fromJson(Map<String,dynamic> json){
//先解析成数组
var temIndexs = json['index'] as List;
//然后把数组中的每个值转成WeatherIndex对象(调用WeatherIndex.fromJson(i))
List<WeatherIndex> indexList = temIndexs.map((i)=>WeatherIndex.fromJson(i)).toList();
var temDailys = json['daily'] as List;
//把数组中的每个值转成WeatherDaily对象(调用WeatherDaily.fromJson(i))
List<WeatherDaily> dailyList = temDailys.map((i)=>WeatherDaily.fromJson(i)).toList();
var temHours = json['hourly'] as List;
//把数组中的每个值转成WeatherHourly对象(调用WeatherHourly.fromJson(i))
List<WeatherHourly> hoursList = temHours.map((i)=>WeatherHourly.fromJson(i)).toList();
return WeatherResult(
city: json['city'],
citycode: json['citycode'].toString(),
...(省略一些)
aqi: Aqi.fromJson(json['aqi']),
indexs: indexList,
dailys: dailyList,
hours: hoursList
);
}
}
解析数组的时候首先将其解析成一个没有指定类型的List,然后遍历数组中的每项数据,将每一项转换成对应的对象。
//先解析成数组
var temIndexs = json['index'] as List;
//然后把数组中的每个值转成WeatherIndex对象(调用WeatherIndex.fromJson(i))
List<WeatherIndex> indexList = temIndexs.map((i)=>WeatherIndex.fromJson(i)).toList();
这里就不在贴出WeatherIndex、WeatherDaily、WeatherHourly的解析了。 可以在下面链接中找到 github.com/Zhengyi66/F…
利用PageController暂时解决滑动冲突
我上面其实在Pageview中有使用PageController的。 因为我们的pageview中嵌套了scrollview,listview和gridview,所以肯定会存在滑动冲突的。使用PageController判断第一个pageview是否滑动完成,即是否已经滑动到第二个页面了。
PageController _pageController = new PageController();
@override
void initState() {
super.initState();
loadWeatherData();
_pageController.addListener((){
//判断第一个pageview是否完成滑动
if( _pageController.position.pixels == _pageController.position.extentInside){
//滑动完成,到第二个页面后。发送消息给第二个页面
eventBus.fire(PageEvent());
}
});
}
FirstPageView实现
pageview中包裹了两个子view,FirstPageView和SecondPageView。 第一个pageview如下:

背景实现

天气信息实现
天气布局 整体可以分为头部,底部和中间的空白。所以使用Column竖直布局来包裹。中间空白使用Expanded填充。
1、头部天气实现

//左边温度信息
Container(width: 200,height: 90,
child: Stack(
alignment: Alignment.center,
fit: StackFit.expand,
children: <Widget>[
Positioned(
child: Text(result.temp,style:
TextStyle(color:Colors.white,fontSize: 90,fontWeight: FontWeight.w200),),
left: 10,
),
Positioned(
child: Text("℃",style: TextStyle(color: Colors.white,fontSize: 20,fontWeight: FontWeight.w300),),
left: 110,
top: 5,
),
Positioned(child: Text(result.weather,
style: TextStyle(color: Colors.white,fontSize: 18),maxLines: 1,overflow: TextOverflow.ellipsis,),
bottom: 5,
left: 110,
)
],
),
),
Stack属性:
- alignment :Alignment.center 对齐方式, 居中
- fit: StackFit.expand, 适应方式 填充
使用Positioned来调整子widget在Stack中的位置 :通过距离 left、top、right、bottom 的距离来确定位置
2、底部信息实现。

SecondPageView实现
布局分析


1、_buildTitle实现
//标题widget
Widget _buildTitle(String title) {
return Container(
padding: EdgeInsets.all(10),
child: Text(
title,
style: TextStyle(color: Colors.white70, fontSize: 16),
),
);
}
就是一个简单的Text。为了复用所以写成方法
2、_buildLine实现
//线widget
Widget _buildLine({double height, Color color}) {
return Container(
height: height == null ? 0.5 : height,
color: color ?? Colors.white,
);
}
就是一个线,可以选择高度和颜色
3、24小时天气实现
//24小时天气widget
Widget _buildHour(List<WeatherHourly> hours) {
List<Widget> widgets = [];
for(int i=0; i<hours.length; i++){
widgets.add(_getHourItem(hours[i]));
}
return Container(
chil(
scrollDirection: Axis.horizontal,
child: Row(
children: widgets,
),
),
);
}
就是一个简单的横向的scrollview。
4、 一周的天气
//多天天气
Widget _buildDaily(List<WeatherDaily> dailys,List<ui.Image> dayImages,List<ui.Image> nightImages){
return Container(
height: 310,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: WeatherLineWidget(dailys, dayImages,nightImages),
),
);
}
可以看到这也是一个简单的Scrollview,里面包裹一个我们自定义的WeatherLineWidget
自定义天气折线图

class WeatherLineWidget extends StatelessWidget {
WeatherLineWidget(this.dailys,this.dayIcons,this.nightIcons);
final List<WeatherDaily> dailys;
final List<ui.Image> dayIcons;
final List<ui.Image> nightIcons;
@override
Widget build(BuildContext context) {
// TODO: implement build
return CustomPaint(
painter: _customPainter(dailys,dayIcons,nightIcons),
size: Size(420, 310),//自定义Widget的宽高
);
}
}
class _customPainter extends CustomPainter {
_customPainter(this.dailys,this.dayImages,this.nightIcons);
List<WeatherDaily> dailys; //数据源
List<ui.Image> dayImages; //白天天气image
List<ui.Image> nightIcons;//夜间天气image
final double itemWidth = 60; //每个item的宽度
final double textHeight = 120; //显示文字的高度
final double temHeight = 80; //温度区域的高度
int maxTem, minTem; //最高/低温度
@override
void paint(Canvas canvas, Size size) async{
}
}
然后在paint()方法中做绘制操作。
1、获得最高最低温度
//设置最高温度,最低温度
setMinMax(){
minTem = maxTem = int.parse(dailys[0].day.temphigh);
for(WeatherDaily daily in dailys){
if(int.parse(daily.day.temphigh) > maxTem){
maxTem = int.parse(daily.day.temphigh);
}
if(int.parse(daily.night.templow) < minTem){
minTem = int.parse(daily.night.templow);
}
}
}
2、绘制文字的方法
//绘制文字
drawText(Canvas canvas, int i,String text,double height,{double frontSize}) {
var pb = ui.ParagraphBuilder(ui.ParagraphStyle(
textAlign: TextAlign.center,//居中
fontSize: frontSize == null ?14:frontSize,//大小
));
//添加文字
pb.addText(text);
//文字颜色
pb.pushStyle(ui.TextStyle(color: Colors.white));
//文本宽度
var paragraph = pb.build()..layout(ui.ParagraphConstraints(width: itemWidth));
//绘制文字
canvas.drawParagraph(paragraph, Offset(itemWidth*i, height));
}
和Android不同的是,Flutter绘制文字使用drawParagraph()
方法
3、paint()方法
@override
void paint(Canvas canvas, Size size) async{
setMinMax();
List<Offset> maxPoints = [];
List<Offset> minPoints = [];
double oneTemHeight = temHeight / (maxTem - minTem); //每个温度的高度
for(int i=0; i<dailys.length; i++){
var daily = dailys[i];
var dx = itemWidth/2 + itemWidth * i;
var maxDy = textHeight + (maxTem - int.parse(daily.day.temphigh)) * oneTemHeight;
var minDy = textHeight + (maxTem - int.parse(daily.night.templow)) * oneTemHeight;
var maxOffset = new Offset(dx, maxDy);
var minOffset = new Offset(dx, minDy);
if(i == 0){
maxPath.moveTo(dx, maxDy);
minPath.moveTo(dx, minDy);
}else {
maxPath.lineTo(dx, maxDy);
minPath.lineTo(dx, minDy);
}
maxPoints.add(maxOffset);
minPoints.add(minOffset);
if(i != 0){
//画竖线
canvas.drawLine(Offset(itemWidth * i ,0), Offset(itemWidth * i, textHeight*2 + textHeight), linePaint);
}
var date;
if(i == 0){
date = daily.week + "\n" + "今天";
}else if(i == 1){
date = daily.week + "\n" + "明天";
}else{
date = daily.week + "\n" + TimeUtil.getWeatherDate(daily.date);
}
//绘制日期
drawText(canvas, i, date ,10);
//绘制白天天气图片 src原始矩阵 dst输出矩阵
canvas.drawImageRect(dayImages[i],Rect.fromLTWH(0, 0, dayImages[i].width.toDouble(), dayImages[i].height.toDouble()),
Rect.fromLTWH(itemWidth/4 + itemWidth*i, 50,30,30),linePaint);
//绘制白天天气
drawText(canvas, i, daily.day.weather, 90);
//绘制夜间天气图片
canvas.drawImageRect(nightIcons[i],Rect.fromLTWH(0, 0, nightIcons[i].width.toDouble(), nightIcons[i].height.toDouble()),
Rect.fromLTWH(itemWidth/4 + itemWidth*i, textHeight + temHeight + 10,30,30),new Paint());
//绘制夜间天气信息
drawText(canvas, i, daily.night.weather, textHeight+temHeight + 45);
//绘制风向和风力
drawText(canvas, i, daily.night.winddirect + "\n" + daily.night.windpower, textHeight+temHeight + 70,frontSize: 10);
}
//最高温度折线
canvas.drawPath(maxPath, maxPaint);
//最低温度折线
canvas.drawPath(minPath, minPaint);
//最高温度点
canvas.drawPoints(ui.PointMode.points, maxPoints, pointPaint);
//最低温度点
canvas.drawPoints(ui.PointMode.points, minPoints, pointPaint);
绘制其实还是挺简单的。注意一下drawImageRect
drawImageRect(Image image, Rect src, Rect dst, Paint paint)
- image是包
'dart:ui'
中的image,不是widget。 - src 原先image的 rect
- dst 输出image 的 rect。可以通过修改此widget的大小达到修改图片大小的效果
加载drawImageRect()中的image
import 'dart:async';
import 'dart:ui' as ui;
import 'dart:typed_data';
initNightIcon(String path) async {
final ByteData data = await rootBundle.load(path);
ui.Image image = await loadNightImage(new Uint8List.view(data.buffer));
}
//加载image
Future<ui.Image> loadNightImage(List<int> img) async {
final Completer<ui.Image> completer = new Completer();
ui.decodeImageFromList(img, (ui.Image img){
return completer.complete(img);
});
return completer.future;
}
pageview的滑动冲突
这是我找到的一种取巧的方式吧,目前不知道官方的方式是啥?
这里面用到了scroll中的一个很关键的属性physics
: ScrollPhysics 滚动系数。
看一下它的实现类:


getScrollPhysics()
方法了么。
//获得滑动系数
ScrollPhysics getScrollPhysics(bool top){
if(top){
return NeverScrollableScrollPhysics();
}else{
return BouncingScrollPhysics();
}
}
top: scrollview是否滑动到顶部。
当scrollview滑动到顶部的时候,physics为NeverScrollableScrollPhysics(),禁止scroll滚动。
当scrollview不在顶部的时候,physics为BouncingScrollPhysics(), 弹性滚动。
下面就是对scrollview的是不是到达顶部的状态监听了。
class _PageState extends State<SecondPageView> {
ScrollController _scrollController = new ScrollController();
bool top = false;
StreamSubscription streamSubscription;
@override
void initState() {
// TODO: implement initState
super.initState();
top = false;
//控制ListView的滑动属性
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
// print("滑动到底部");
} else if (_scrollController.position.pixels ==
_scrollController.position.minScrollExtent) {
// print("滑动到顶部");
setState(() {
top = true;
});
} else {
top = false;
}
});
//接收pageview的滑动事件,此时page已经滑动到第二个页面了,修改physics属性
streamSubscription = eventBus.on<PageEvent>().listen((event) {
setState(() {
top = false
;
});
});
}
@override
void dispose() {
top = false;
if (streamSubscription != null) {
streamSubscription.cancel();
}
super.dispose();
}
}
通过_scrollController
和注册的pageview的滚动事件一起来确定scrollview是否可以滚动。
结束
这里就是天气模块的内容了,完整代码已经上传到GitHub上了。github.com/Zhengyi66/F…