Flutter Weather天气模块实现

作者: 深情不及酒伴 | 来源:发表于2019-07-31 19:55 被阅读2次

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

    在这里插入图片描述
    项目地址:https://github.com/Zhengyi66/Flutter_weather

    首页最外层布局实现

    首页包含一个顶部的城市名称展示栏和一个pageview。因此可以使用一个Column竖直的列进行包裹。

        return Container(
          child: Column(
            children: <Widget>[
              //头
              buildBar(context),
              //pageview
              Expanded(child: _buildPageView(),
              )
            ],
          ),
        );
    

    使用Expanded填充剩余空间,类似Android权重属性。

    PageView实现

    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

    数据加载

    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的解析了。
    可以在下面链接中找到 https://github.com/Zhengyi66/Flutter_weather/blob/master/lib/model/weather_bean.dart

    利用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如下:


    first

    一张充满屏幕的背景图片和上下两部分的天气信息。

    背景实现
    背景

    使用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 滚动系数。
    看一下它的实现类:

    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上了。https://github.com/Zhengyi66/Flutter_weather

    相关文章

      网友评论

        本文标题:Flutter Weather天气模块实现

        本文链接:https://www.haomeiwen.com/subject/eekxdctx.html