本文介紹Flutter_Weather天氣模組實現。效果圖如下:
完整專案地址:github.com/Zhengyi66/F…首頁最外層佈局實現
首頁包含一個頂部的城市名稱展示欄和一個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的特性
頂部標題欄實現
如上圖,橫向排列的3個widget,可以使用Row進行包裹。使用GestureDetector為其增加點選事件。程式碼如下: 選擇城市之後我們需要接收選擇城市的返回值,所以我們需要接受路由的回撥Future,並新增它的回撥方法,在回撥方法中獲得返回的城市然後重新載入資料。類似Android activityresult //跳轉頁面並接收返回值
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如下:
一張充滿螢幕的背景圖片和上下兩部分的天氣資訊。背景實現
使用Stack實現佈局的層級巢狀,背景在最底層,天氣資訊在上層。 Stack的 fit屬性要設定StackFit.expand填充,不然圖片不會充滿全屏。天氣資訊實現
天氣佈局 整體可以分為頭部,底部和中間的空白。所以使用Column豎直佈局來包裹。中間空白使用Expanded填充。
1、頭部天氣實現
最外層是一個橫向排列的Row佈局,中間使用Expanded填充。 左邊黃色框內內容使用Column包裹。Column中包含一個Stack和一個Container。 因為這個頁面用了很多Stack佈局,所以展示一個藍色框內Stack的實現: //左邊溫度資訊
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、底部資訊實現。
底部佈局就是一個Row和兩個相同的Stack。為了使左右連個Stack能夠平分寬度,可以使用Expanded進行包裹。 Expand有個屬性flex預設為1,類似Android的權重。SecondPageView實現
佈局分析
如上圖。最外層是一個Stack,裡面包裹一個背景圖片,圖片的上面是一個ScrollView(也可以是ListView 最開始用的就是listview,但是用了listview上面會有一小段空白,listview不能充滿全屏,應該是我佈局時候出來點毛病吧。) 然後ScrollView中包裹一個Column。程式碼如下 因為這裡有一個載入assets中image的過程,所以加一個imageLoaded圖片是否載入完成的判斷。載入完成才顯示內容。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…