参考:https://coding.imooc.com/class/321.html
最终效果:
上源码:如有纰漏,敬请指出~
import 'package:doucan_flutter/model/navi/home_grid_nav_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_color_plugin/flutter_color_plugin.dart';
const DEF_ICON = '....png';
/// 仿携程网格导航模块
/// UI分析:
/// 1、该模块四处有圆角:PhysicalModel
/// 2、由三组横向排列的模块纵向排列构成:column包裹3个row
/// 3、横向模块,有三个子模块等分排列:子模块均用Expanded-flex=1包裹
/// 4、左边子模块:背景图片+文字在上层,Stack
/// 5、中间子模块:两个文字上下排列,且均有左边框,上门的文字有底边框:borderSide
/// 6、右边子模块,和中间子模块一样
class GridNav extends StatefulWidget {
final List<ModelsData> models;
const GridNav({Key? key, required this.models}) : super(key: key);
@override
_GridNavState createState() => _GridNavState();
}
class _GridNavState extends State<GridNav> {
@override
Widget build(BuildContext context) {
//使用PhysicalModel对最终的布局Widget进行圆角裁剪
return PhysicalModel(
color: Colors.transparent,
borderRadius: BorderRadius.circular(10),
clipBehavior: Clip.antiAlias,
//纵向排列的,直接使用column。一般都不需要加出child外的属性(长度如何计算?自动根据子布局撑开?)
child: Column(
children: _rowItems(widget.models),
),
);
}
List<Widget> _rowItems(List<ModelsData> models) {
List<Widget> rowItems = [];
if (models == null || models.isEmpty) {
return rowItems;
}
models.forEach((model) {
rowItems.add(_rowItem(model, models.indexOf(model) == models.length - 1));
});
return rowItems;
}
/// isEnd -- 判断当前items是否为最后一个,若是,则不再添加底部的margin,防止PhysicalModel裁剪不正确
Widget _rowItem(ModelsData model, isEnd) {
List<Widget> items = [];
items.add(_leftItem(model.items[0]));
items.add(_middleItem(model.items[1], model.items[2]));
items.add(_rightItem(model.items[3], model.items[4]));
List<Widget> expendItems = [];
items.forEach((element) {
//因为横向布局的三个子布局宽度均分,所以使用Expanded包裹每一个子布局
expendItems.add(Expanded(
child: element,
// flex: 1,
));
});
//一般横向布局使用container包裹,便于设置宽高、内外边距等
return Container(
//横向布局间有间隙,container中添加底部margin即可
margin: isEnd ? EdgeInsets.only(bottom: 0) : EdgeInsets.only(bottom: 2),
//横向布局的高度需要确定,根据UI设计稿来设置,不然图片大小不好处理
height: 88,
//添加线性渐变的背景色
decoration: BoxDecoration(
gradient: LinearGradient(colors: [
ColorUtil.color(model.startColor),
ColorUtil.color(model.endColor)
])),
child: Row(
children: expendItems,
),
);
}
Widget _leftItem(ItemsData item) {
return _wrapGesture(
item,
//该布局为底部是图片,顶部是文字,层叠而成
Stack(
//文字是顶部居中显示的
alignment: Alignment.topCenter,
children: [
Image.network(
item.icon == '' ? DEF_ICON : item.icon,
//宽高是根据设计稿设置的,设计稿里面是一副(88*120)(单位点)的透明背景图片
height: 88,
width: 121,
//图片需要完全显示出来
fit: BoxFit.contain,
//实际图片可能没有设置的图片的宽高,设置该属性可以避免图片没有贴底显示
alignment: AlignmentDirectional.bottomEnd,
),
//文字层叠在图片上面
Container(
margin: EdgeInsets.only(top: 11),
child: Text(
item.title,
style: TextStyle(fontSize: 14, color: Colors.white),
),
)
],
));
}
Widget _middleItem(ItemsData item1, ItemsData item2) {
///中间的布局是两个等高的文字竖排
///使用Column竖向排列,高度如何计算的?
///使用Expanded包裹每一个子布局,平分Column竖向的空间
return Column(
children: [
Expanded(child: _item(item1, true)),
Expanded(child: _item(item2, false)),
],
);
}
Widget _rightItem(ItemsData item1, ItemsData item2) {
return Column(
children: [
Expanded(child: _item(item1, true)),
Expanded(child: _item(item2, false)),
],
);
}
//布局中有多个需要点击的部分,且点击逻辑一致,故进行封装
Widget _wrapGesture(ItemsData item, child) {
return GestureDetector(
onTap: () {
print(item.title);
},
child: child,
);
}
///文字item布局
_item(ItemsData item, bool isTop) {
BorderSide borderSide = BorderSide(width: 0.8, color: Colors.white);
//文字item布局的父布局高度已经撑好了(宽度应该也撑好了吧?)
//但Container是自适应子元素的,而子元素的Text宽度是自适应文字的
//故,要使用FractionallySizedBox把Container的宽度撑开-撑满
return FractionallySizedBox(
widthFactor: 1,
child: Container(
//子元素居中(当我们要把子元素居于哪个位置时,使用该属性)
alignment: Alignment.center,
decoration: BoxDecoration(
//添加边框,营造表格感:控制好添加那条边的逻辑
border: Border(
left: borderSide,
bottom: isTop ? borderSide : BorderSide.none,
)),
child: _wrapGesture(
item,
Text(
item.title,
//父布局的Container已经设置了Alignment,就由父布局来控制文字的位置好了
//若父布局的Container没设置了Alignment,文字只能水平居中显示,达不到垂直水平居中效果
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.white),
)),
),
);
}
}
导航菜单json:
{
"models": [{
"startColor": "5EFCE8",
"endColor": "736EFE",
"items": [{
"title": "酒店",
"icon": "",
"url": ""
}, {
"title": "海外",
"icon": "",
"url": ""
}, {
"title": "特价",
"icon": "",
"url": ""
}, {
"title": "团购",
"icon": "",
"url": ""
}, {
"title": "民宿",
"icon": "",
"url": ""
}]
}, {
"startColor": "43CBFF",
"endColor": "9708CC",
"items": [{
"title": "机票",
"icon": "",
"url": ""
}, {
"title": "火车票·高铁票",
"icon": "",
"url": ""
}, {
"title": "特价",
"icon": "",
"url": ""
}, {
"title": "专车快车",
"icon": "",
"url": ""
}, {
"title": "船票",
"icon": "",
"url": ""
}]
}, {
"startColor": "EA5455",
"endColor": "FEB692",
"items": [{
"title": "旅游",
"icon": "",
"url": ""
}, {
"title": "门票",
"icon": "",
"url": ""
}, {
"title": "攻略",
"icon": "",
"url": ""
}, {
"title": "游轮",
"icon": "",
"url": ""
}, {
"title": "定制",
"icon": "",
"url": ""
}]
}]
}
import 'package:json_annotation/json_annotation.dart';
part 'home_grid_nav_model.g.dart';
@JsonSerializable(explicitToJson: true)
class Homegridnavmodel {
HomegridnavmodelData data;
int code;
String message;
Homegridnavmodel(this.data, this.code, this.message);
factory Homegridnavmodel.fromJson(Map<String, dynamic> json) =>
_$HomegridnavmodelFromJson(json);
Map<String, dynamic> toJson() => _$HomegridnavmodelToJson(this);
}
@JsonSerializable(explicitToJson: true)
class HomegridnavmodelData {
List<ModelsData> models;
HomegridnavmodelData(
this.models,
);
factory HomegridnavmodelData.fromJson(Map<String, dynamic> json) =>
_$HomegridnavmodelDataFromJson(json);
Map<String, dynamic> toJson() => _$HomegridnavmodelDataToJson(this);
}
@JsonSerializable(explicitToJson: true)
class ModelsData {
String startColor;
String endColor;
List<ItemsData> items;
ModelsData(this.items, this.startColor, this.endColor);
factory ModelsData.fromJson(Map<String, dynamic> json) =>
_$ModelsDataFromJson(json);
Map<String, dynamic> toJson() => _$ModelsDataToJson(this);
}
@JsonSerializable(explicitToJson: true)
class ItemsData {
String title;
String icon;
String url;
ItemsData(
this.title,
this.icon,
this.url,
);
factory ItemsData.fromJson(Map<String, dynamic> json) =>
_$ItemsDataFromJson(json);
Map<String, dynamic> toJson() => _$ItemsDataToJson(this);
}
json解析部分的代码就省略了,不是重点。