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

前言

本文部分代码参考了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 createState() {
    return DeliverInfoPageState(trackingNum);
  }
}

class DeliverInfoPageState extends State{

  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: [
        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: [
                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: [
                      Expanded(
                        flex: 1,
                        child: Container(
                          margin: EdgeInsets.only(left: 10),
                          alignment: Alignment.centerLeft,
                          child: Row(
                            children: [
                              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: [
                      //为了实现类似阴影的圆点
                      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实现类似物流的时间轴(详细))