前言
本文部分代码参考了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": [] //自动识别结果
}
}
结束语
这个代码复制应该可以直接运行的,除了我封装的两个类,其中一个已经在注释中给出了,另一个打算写在对阿里云查询接口具体使用的博客中(如果我不鸽的话,会更新这个帖子的超链接的!)