美文网首页
Flutter 使用ListView实现类似物流的时间轴(详细)

Flutter 使用ListView实现类似物流的时间轴(详细)

作者: _深巷丶 | 来源:发表于2020-09-18 11:25 被阅读0次

    前言

    本文部分代码参考了Flutter 类似物流的 时间轴 ListView 时间轴 - 简书 ,前排感谢。
    使用的接口是阿里云的:易源数据-快递物流查询API接口,具体使用和一些细节打算专门再写一个博客。
    最先发布于俺的CSDN博客,欢迎赏脸:Flutter 使用ListView实现类似物流的时间轴(详细)
    简书的Markdown居然不识别[TOC]语法,不能自动生成目录!

    效果图

    先上效果图,手机截图略大,见谅

    效果图

    具体代码

    直接看代码吧,讲解啥的都写在注释里了,有点啰嗦,想尽量讲的详细一点,莫怪莫怪

    import 'dart:convert';
    
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    
    class DeliverInfoPage extends StatefulWidget{
    
      //从上一个页面传过来的快递单号
      String trackingNum;
    
      DeliverInfoPage(this.trackingNum);
    
      @override
      State<StatefulWidget> createState() {
        return DeliverInfoPageState(trackingNum);
      }
    }
    
    class DeliverInfoPageState extends State<DeliverInfoPage>{
    
      String trackingNum;
      //get请求获取的数据
      Map jsonMap;
    
      DeliverInfoPageState(this.trackingNum);
    
      @override
      void initState() {
        //NetInterface是自己封装的网络接口类,把项目中用到的接口都放在一起,便于管理
        //对于阿里云接口的具体使用看另一个帖子吧。毕竟不是所有人都用的这个,就不在这里展开了
        NetInterface.getDeliverInfo(trackingNum).then((response) {
    //      print("getDeliverInfo=>"+response.toString());
          //jsonMap的具体格式请看阿里云API购买页面,本博最后也会贴出来
          jsonMap = json.decode(response.toString());
          setState(() { });
        }).catchError((response) {
          //ToastUtil也是封装的一个类,具体代码是:
          /*class ToastUtil{
            static void print(String msg){
              Fluttertoast.showToast(
              msg: msg,
              toastLength: Toast.LENGTH_SHORT,
              gravity: ToastGravity.CENTER,
              timeInSecForIosWeb: 1,
              );
            }
          }*/
          ToastUtil.print("出现错误,请重试");
          print("getDeliverInfo Error=>"+response.toString());
        });
      }
    
      @override
      Widget build(BuildContext context) {
        //因为这个项目是安卓和flutter混合开发,所以用了WillPopScope拦截退出事件
        return WillPopScope(
          child: Scaffold(
            appBar: AppBar(
              title: Text("物流追踪"),
              leading: IconButton(
                  icon: Icon(Icons.arrow_back),
                  onPressed: () {
                    Navigator.pop(context);
                  }
              ),
            ),
            //未获取到数据就居中显示加载图标
            body: jsonMap != null ?  buildBody(context) : showLoading(),
          ),
          onWillPop: (){
            Navigator.pop(context);
          },
        );
      }
    
      Widget buildBody(BuildContext context){
        return Column(
          children: <Widget>[
            Container(
              padding: EdgeInsets.fromLTRB(10, 0, 0, 0),
              width: double.infinity,
              color: Colors.white,
              height: 70,
              child: Container(
                margin: EdgeInsets.all(5),
                child: Row(
                  children: <Widget>[
                    Container(
                      height: 60,
                      width: 60,
                      margin: EdgeInsets.fromLTRB(5, 5, 10, 5),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(50),
                        child: FadeInImage.assetNetwork(
                          //用了一个加载中的GIF作为默认占位图
                          //注意图片要在pubspec.yaml声明一下,我刚写的时候忘了,就无法加载
                          placeholder: "assets/loading.gif",
                          image: jsonMap["showapi_res_body"]["logo"],
                          fit: BoxFit.fitWidth,
                        ),
                      ),
                    ),
                    Expanded(
                      child: Column(
                        children: <Widget>[
                          Expanded(
                            flex: 1,
                            child: Container(
                              margin: EdgeInsets.only(left: 10),
                              alignment: Alignment.centerLeft,
                              child: Row(
                                children: <Widget>[
                                  Text("物流状态:",style: TextStyle(fontSize: 16)),
                                  Text(
                                      "${statusConvert(jsonMap["showapi_res_body"]["status"])}", 
                                      style: TextStyle(fontSize: 16, color: Colors.green)
                                  ),
                                ],
                              ),
                            ),
                          ),
                          Expanded(
                            flex: 1,
                            child: Container(
                              margin: EdgeInsets.only(left: 10),
                              alignment: Alignment.centerLeft,
                              child: Text(
                                  "运单编号:$trackingNum", 
                                  style: TextStyle(
                                      fontSize: 15, 
                                      //颜色稍淡一点
                                      color: Color.fromARGB(95, 0, 0, 0)
                                  )
                              ),
                            ),
                          ),
                        ],
                      ),
                    )
                  ],
                ),
              ),
            ),
            buildListView(context, jsonMap["showapi_res_body"]["data"]),
          ],
        );
      }
    
      Widget buildListView(BuildContext context, List list){
        return Expanded(
          child: Container(
            margin: EdgeInsets.fromLTRB(0, 10, 0, 0),
            color: Colors.white,
            child: ListView.builder(
                //想设置item为固定高度可以设置这个,不过文本过长就显示不全了
    //            itemExtent: 100,
                itemCount: list == null ? 0 : list.length,
                itemBuilder: (BuildContext context, int position){
                  return buildListViewItem(context, list, position);
                }
            ),
          ),
        );
      }
    
      Widget buildListViewItem(BuildContext context, List list, int position){
        if(list.length != 0){
          return Container(
            color: Colors.white,
            padding: EdgeInsets.only(left: 20, right: 10),
            child: Row(
              children: [
                //这个Container描绘左侧的线和圆点
                Container(
                  margin: EdgeInsets.only(left: 10),
                  width: 20,
                  //需要根据文本内容调整高度,不然文本太长会撑开Container,导致线会断开
                  height: getHeight(list[position]["context"]),
                  child: Column(
                    //中心对齐,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Expanded(
                          flex: 1,
                          child: Container(
                            //第一个item圆点上方的线就不显示了
                            width: position == 0 ? 0 : 1,
                            color: Colors.grey,
                          )
                      ),
                      //第一个item显示稍大一点的绿色圆点
                      position == 0 ? Stack(
                        //圆心对齐(也就是中心对齐)
                        alignment: Alignment.center,
                        children: <Widget>[
                          //为了实现类似阴影的圆点
                          Container(
                            height: 20,
                            width: 20,
                            decoration: BoxDecoration(
                              color: Colors.green.shade200,
                              borderRadius: BorderRadius.all(Radius.circular(10)),
                            ),
                          ),
                          Container(
                            height: 14,
                            width: 14,
                            decoration: BoxDecoration(
                              color: Colors.green,
                              borderRadius: BorderRadius.all(Radius.circular(7)),
                            ),
                          ),
                        ],
                      ) : Container(
                        height: 10,
                        width: 10,
                        decoration: BoxDecoration(
                          color: Colors.grey.shade300,
                          borderRadius: BorderRadius.all(Radius.circular(5)),
                        ),
                      ),
                      Expanded(
                          flex: 2,
                          child: Container(
                            width: 1,
                            color: Colors.grey,
                          )
                      ),
                    ],
                  ),
                ),
                Expanded(
                  child: Padding(
                    padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
                    child: Text(
                      list[position]["context"] + "\n" + list[position]["time"],
                      style: TextStyle(
                        fontSize: 15,
                        //第一个item字体颜色为绿色+稍微加粗
                        color: position == 0 ? Colors.green : Colors.black,
                        fontWeight: position == 0 ? FontWeight.w600 : null,
                      ),
                    ),
                  ),
                ),
              ],
            ),
          );
        }else{
          return Container();
        }
      }
    
      Widget showLoading(){
        return Center(
          child: CupertinoActivityIndicator(
            radius: 20,
          ),
        );
      }
    
      double getHeight(String content){
        //具体多长的文字需要增加高度,看手机分辨率和margin、padding的设置了
        if(content.length >= 95){
          return 150;
        } else if(content.length >= 80 && content.length < 95){
          return 130;
        } else if(content.length >= 40 && content.length < 80){
          return 110;
        } else if(content.length >= 20 && content.length < 40){
          return 90;
        } else {
          return 70;
        }
      }
    
      //把int类型的状态转成字符串,具体对应请看阿里云API购买页面,本博最后的图也会有
      String statusConvert(int status){
        String returnStatus;
        switch(status) {
          case -1: { returnStatus = "待查询"; }
          break;
          case 0: { returnStatus = "查询异常"; }
          break;
          case 1: { returnStatus = "暂无记录"; }
          break;
          case 2: { returnStatus = "在途中"; }
          break;
          case 3: { returnStatus = "派送中"; }
          break;
          case 4: { returnStatus = "已签收"; }
          break;
          case 5: { returnStatus = "用户拒签"; }
          break;
          case 6: { returnStatus = "疑难件"; }
          break;
          case 7: { returnStatus = "无效单"; }
          break;
          case 8: { returnStatus = "超时单"; }
          break;
          case 9: { returnStatus = "签收失败"; }
          break;
          case 10: { returnStatus = "退回"; }
          break;
          default: { returnStatus = "未知状态"; }
          break;
        }
        return returnStatus;
      }
    }
    
    

    返回数据的结构

    这个实际就是易源数据-快递物流查询API接口的,不是泄露别人隐私哈
    注意API接口是这个,别看错了

    接口选择
    {
      "showapi_res_error": "",//showapi平台返回的错误信息
      "showapi_res_code": 0,//showapi平台返回码,0为成功,其他为失败
      "showapi_res_id": "5ea941d48d57baae12a0bcd5",
      "showapi_res_body": {
        "update": 1588141785719,//数据最后查询的时间
        "upgrade_info": "", //提示信息,用于提醒用户可能出现的情况
        "updateStr": "2020-04-29 14:29:45",//数据最后更新的时间
        "logo": "http://app2.showapi.com/img/expImg/zto.jpg", //快递公司logo
        "dataSize": 11,  //数据节点的长度
        "status": 4, //-1 待查询 0 查询异常 1 暂无记录 2 在途中 3 派送中 4 已签收 5 用户拒签 6 疑难件 7 无效单 8 超时单 9 签收失败 10 退回
        "fee_num": 1,
        "tel": "95311",//快递公司电话
        "data": [
          {
            "time": "2019-11-16 21:33:56",
            "context": "快件已在 【九江城西港】 签收, 签收人: 速递易, 如有疑问请电联:(15779254414), 投诉电话:(13687028760), 您的快递已经妥投。风里来雨里去, 只为客官您满意。上有老下有小, 赏个好评好不好?【请在评价快递员处帮忙点亮五颗星星哦~】"
          },
          {
            "time": "2019-11-16 07:31:24",
            "context": "【九江城西港】 的程继业(15779254414) 正在第1次派件, 请保持电话畅通,并耐心等待(95720为中通快递员外呼专属号码,请放心接听)"
          },
          {
            "time": "2019-11-16 07:31:23",
            "context": "快件已经到达 【九江城西港】"
          },
          {
            "time": "2019-11-15 19:06:30",
            "context": "快件离开 【九江】 已发往 【九江城西港】"
          },
          {
            "time": "2019-11-15 19:06:18",
            "context": "快件已经到达 【九江】"
          },
          {
            "time": "2019-11-15 10:45:21",
            "context": "快件离开 【南昌中转部】 已发往 【九江】"
          },
          {
            "time": "2019-11-15 08:02:44",
            "context": "快件已经到达 【南昌中转部】"
          },
          {
            "time": "2019-11-13 15:19:48",
            "context": "快件离开 【石家庄】 已发往 【南昌中转部】"
          },
          {
            "time": "2019-11-13 14:22:09",
            "context": "快件已经到达 【石家庄】"
          },
          {
            "time": "2019-11-13 14:08:31",
            "context": "快件离开 【石家庄市场部】 已发往 【石家庄】"
          },
          {
            "time": "2019-11-13 10:27:33",
            "context": "【石家庄市场部】(0311-68026565、0311-68026566) 的 付保文四组(031186891089) 已揽收"
          }
        ],
        "expSpellName": "zhongtong",//快递字母简称
        "msg": "查询成功", //返回提示信息
        "mailNo": "75312165465979",//快递单号
        "queryTimes": 1, //无走件记录时被查询次数     注意:超过8次将会计费,即第9次开始计费
        "ret_code": 0,//接口调用是否成功,0为成功,其他为失败
        "flag": true,//物流信息是否获取成功
        "expTextName": "中通快递", //快递简称
        "possibleExpList": [] //自动识别结果
      }
    }
    

    结束语

    这个代码复制应该可以直接运行的,除了我封装的两个类,其中一个已经在注释中给出了,另一个打算写在对阿里云查询接口具体使用的博客中(如果我不鸽的话,会更新这个帖子的超链接的!)

    相关文章

      网友评论

          本文标题:Flutter 使用ListView实现类似物流的时间轴(详细)

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