1.旅拍界面展示

2.界面布局

  • 顶部是 TabBar 配合 TabBarView 实现页面滑动翻页
  • TabBarView 用 Flexible 包裹 Flexible 包裹充满整个页面
  • 内容区部分采用 StaggeredGridView 构建瀑布流式布局,引入插件 flutter_staggered_grid_view
  • 自定义 LoadingContainer 在进入界面的时候弹出一个加载菊花。
  • 通过 RefreshIndicator 控件实现下拉刷新。
  • 通过 MediaQuery.removePadding 移除导航栏间距。

TravelPage 页面布局如下,顶部的 TabBar 切换

///旅拍主界面
class TravelPage extends StatefulWidget {TravelPage({Key? key}) : super(key: key);@override_TravelPageState createState() => _TravelPageState();
}class _TravelPageState extends State<TravelPage>with TickerProviderStateMixin {// List<String> tabs = ["推荐", "附近", "热门", "旅行热点", "露营初体验", "酒店民宿",//   "美食探店", "亲子", "小众", "自驾", "网红", "逛展"];List<TravelTab> tabs = [];TravelTabModel? travelTabModel;late TabController _controller;@overridevoid initState() {print("TravelPage initState...");super.initState();_controller = TabController(length: tabs.length, vsync: this);_loadData();}@overrideWidget build(BuildContext context) {return Scaffold(body: Column(children: [Container(color: Colors.white,padding: const EdgeInsets.only(top: 30),child: TabBar(controller: _controller,isScrollable: true,labelColor: Colors.black,labelPadding: const EdgeInsets.fromLTRB(20, 0, 20, 5),indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Color(0xff1fcfbb), width: 3),insets: EdgeInsets.only(bottom: 10),),tabs: tabs.map<Tab>((TravelTab tab) {return Tab(text: tab.labelName);}).toList()),),Flexible(child: TabBarView(controller: _controller,children: tabs.map<TravelItemPage>((TravelTab tab) {return TravelItemPage(travelUrl: travelTabModel!.url,params: travelTabModel!.params,groupChannelCode: tab.groupChannelCode,type: tab.type,);}).toList(),))],),);}@overridevoid dispose() {_controller.dispose();super.dispose();}//初始化tab数据void _loadData() async {try {TravelTabModel model = await TravelTabDao.fetch();_controller = TabController(length: model.tabs.length, vsync: this); // fix tab label 空白问题setState(() {tabs = model.tabs;travelTabModel = model;});} catch (e) {print(e);}}
}

Body 数据展示部分布局:

  • 最外层是一个 Card 布局,支持设置阴影,形状等,如图中的 5 所示。内部用一个PhysicalModel用于裁剪圆角等,设置裁剪透明。
  • 内部放置一个 Column 布局,上面显示图片等,下面显示描述信息和用户信心。
  • 如图标注 1 应该是一个 Stack 布局,放置一个 Image 和 一个绝对位置的 Positioned =》放置一个 Container =》 Row =》Padding +LimitedBox
  • 如图标注 3 是一个 Container =》Text
  • 如图标注 4 是 Container =》 Row =》PhysicalModel + Container +Row

完整代码如下:

///构建每个小卡片的样式
class _TravelItem extends StatelessWidget {final TravelItem item;final int index;const _TravelItem({Key? key, required this.item, required this.index}): super(key: key);@overrideWidget build(BuildContext context) {return GestureDetector(onTap: () {},child: Card(child: PhysicalModel(color: Colors.transparent,child: Column(crossAxisAlignment: CrossAxisAlignment.start, //左对齐children: [_itemImage,Container(padding: const EdgeInsets.all(4),child: Text(item.article.articleTitle,maxLines: 2,overflow: TextOverflow.ellipsis,style: const TextStyle(fontSize: 14, color: Colors.black87),),),_infoText],),),),);}String _poiName() {return item.article.pois.isEmpty ? '未知' : item.article.pois[0].poiName;}///卡片布局中的图片样式,采用 Stack 控件,图片加文字等Widget get _itemImage {return Stack(children: [CachedImage(imageUrl: item.article.images[0].dynamicUrl),//在图片上方放置一个绝对位置的布局Positioned(bottom: 8,left: 8,child: Container(padding: const EdgeInsets.fromLTRB(5, 1, 5, 1),decoration: BoxDecoration(color: Colors.black54, borderRadius: BorderRadius.circular(10)),child: Row(children: [const Padding(padding: EdgeInsets.only(right: 3),child: Icon(Icons.location_on,color: Colors.white,size: 12,)),//限制子控件大小的 WidgetLimitedBox(maxWidth: 130,child: Text(_poiName(),maxLines: 1,overflow: TextOverflow.ellipsis, //尾部截断style: const TextStyle(fontSize: 12, color: Colors.white),),),],),)),]);}//图片下面的用户信息展示Widget get _infoText {return Container(padding: const EdgeInsets.fromLTRB(6, 0, 6, 8),child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,children: [PhysicalModel(color: Colors.transparent,clipBehavior: Clip.antiAlias,borderRadius: BorderRadius.circular(12),child: CachedImage(imageUrl: item.article.author.coverImage.dynamicUrl,width: 24,height: 24,),),Container(padding: const EdgeInsets.all(5),width: 90,child: Text(item.article.author.nickName,maxLines: 1,overflow: TextOverflow.ellipsis,style: const TextStyle(fontSize: 12),),),Row(children: [const Icon(Icons.thumb_up,size: 14,color: Colors.grey,),Padding(padding: const EdgeInsets.only(left: 3),child: Text(item.article.likeCount.toString(),style: const TextStyle(fontSize: 10),),)],)],),);}
}

整个页面布局如下:

const TRAVEL_URL ='https://m.ctrip.com/restapi/soa2/16189/json/searchTripShootListForHomePageV2?_fxpcqlniredt=09031014111431397988&__gw_appid=99999999&__gw_ver=1.0&__gw_from=10650013707&__gw_platform=H5';
const PAGE_SIZE = 10;/// 旅拍视图展示界面 body 部分,瀑布流式布局
class TravelItemPage extends StatefulWidget {final String travelUrl;final Map params;final String groupChannelCode;final int type;const TravelItemPage({Key? key,required this.travelUrl,required this.params,required this.groupChannelCode,required this.type}): super(key: key);@override_TravelItemPageState createState() => _TravelItemPageState();
}class _TravelItemPageState extends State<TravelItemPage>with AutomaticKeepAliveClientMixin {//数据列表List<TravelItem> travelItems = [];//分页索引int pageIndex = 1;//是否正在加载bool _loading = true;@overridebool get wantKeepAlive => true;final ScrollController _scrollController = ScrollController();@overridevoid initState() {super.initState();_loadData();}@overridevoid dispose() {_scrollController.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return Scaffold(body: LoadingContainer(isLoading: _loading,child: RefreshIndicator(onRefresh: _handleRefresh,child: MediaQuery.removePadding(context: context,child: StaggeredGridView.countBuilder(controller: _scrollController,itemCount: travelItems.length,crossAxisCount: 2,itemBuilder: (BuildContext context, int index) =>_TravelItem(index: index, item: travelItems[index]),staggeredTileBuilder: (int index) =>const StaggeredTile.fit(1)),removeTop: true,),),),);}/// 下拉刷新Future _handleRefresh() async {_loadData();}/// 加载数据,是下拉刷新还是加载更多void _loadData({loadMore = false}) async {if (loadMore) {pageIndex++;} else {pageIndex = 1;}try {TravelModel model = await TravelDao.fetch(widget.travelUrl, widget.params,widget.groupChannelCode, widget.type, pageIndex, PAGE_SIZE);setState(() {print(model.totalCount);List<TravelItem> items = model.resultList;if (travelItems.isNotEmpty) {travelItems.addAll(items);} else {travelItems = items;}_loading = false;});} catch (e) {print(e);setState(() {_loading = false;});}}
}

项目源码

  • 源码地址:Gihut
  • 该篇 git 提交记录为:旅拍界面布局实现。

Flutter开发学习课程携程app开发(二)相关推荐

  1. Flutter开发学习课程携程app开发(完)

    1.Flutter 列表选择器插件 1. 推荐插件:azlistview Flutter 城市列表.联系人列表,索引&悬停.基于scrollable_positioned_list.AzLis ...

  2. Flutter开发学习课程携程app开发(一)

    引言 数据来源mooc网学习视频课程. 1.页面数据 1.数据展示 接口地址: json 格式 对应的数据界面展示效果: 2.数据请求 应用Dio进行数据请求:home_dao.dart 在 配置文件 ...

  3. Flutter开发学习课程携程app开发(三)

    1.效果展示 需要实现的功能: 自定义一个 SearchBar, 它在主页和搜索页会呈现不同的状态显示. 在搜索框中无输入的时候显示一个语音小图标,有输入的时候显示一个清除图标. 在 HomePage ...

  4. RN开发实践——仿携程App(二)

    文章最后附上源码地址 上一片博客链接RN开发实践--仿携程App(一) 实现首页的轮播图 Swiper简介 The best Swiper component for React Native.Swi ...

  5. RN开发实践——仿携程App(三)

    文章最后附上源码地址 上一片博客链接RN开发实践--仿携程App(二) 实现首页中间的内容栏 今天实现首页中间的内容栏,原效果如下: 红框就是今天需要实现的内容 这里可以拆解成四个部分,每个部分都是由 ...

  6. RN开发实践——仿携程App(一)

    文章最后附上项目地址. 1.新建项目 在控制台执行下面命令( 前提是已经搭建好react-native的开发环境 ): react-native init XieCheng // XieCheng 是 ...

  7. 干货 | 携程APP Native/RN内嵌Flutter UI混合开发实践和探索

    作者简介 Deway,携程资深工程师,iOS客户端开发,热衷于大前端和动态化技术: Frank,携程高级工程师,关注移动端热门技术,安卓客户端开发. 前言 随着各种多端技术的蓬勃发展,如今的移动端和前 ...

  8. 3星|《大产品,小团队》:携程软件开发流程改进的故事

    大产品,小团队:携程敏捷技术与管理转型实战 携程集团创作,作者有产品.开发.测试.PMO等多种角色.有一点比较怪异,每个章节的作者是放在书的最后部分的. 主要内容是携程的软件开发流程改进的故事.携程的 ...

  9. php携程 线程,携程api开发解决方法

    携程api开发 本帖最后由 lziyanl 于 2014-06-03 13:53:29 编辑 如何获取上图的内容信息?在携程没找到对应接口,询问官方群,基本不搭理! ------解决方案------- ...

最新文章

  1. 异常The Struts dispatcher cannot be found. This is
  2. react-native侧滑
  3. 小项目--bank1
  4. “七层架构”-----实践篇-登录小实例
  5. 单元测试 | 如何在Mock时匹配匿名类型参数
  6. linux捕捉信号sigint失败,为shell布置陷阱:trap捕捉信号方法论
  7. java web项目中的根路径踩坑
  8. 基于SpringBoot的项目管理后台
  9. 计算机科技作品大赛,世界编程大赛一等奖作品
  10. qtqpixmap不出现图片_亚马逊对产品图片有哪些基本要求
  11. web安全设置(含IIS,php,ASP.NET)与目录权限设置
  12. python编辑程序用print函数输出中国加油武汉加油_python练习1之print函数
  13. python三维数据欠采样_数据分析:使用Imblearn处理不平衡数据(过采样、欠采样)...
  14. 利用Multipart上传文件报错:The field fileUpload exceeds its maximum permitted size of 1048576 bytes
  15. Pyinstaller:moviepy打包报错AttributeError: module ‘moviepy.audio.fx.all‘ has no attribute ‘audio_fadein‘
  16. 深入了解示波器(三):示波器的带宽
  17. 2023编程语言趋势
  18. 深度分析:一次Wi-Fi入侵实录(1)
  19. StringBuilder.AppendFormat(String, Object, Object) 方法
  20. 全球及中国数字每周可编程时间开关行业研究及十四五规划分析报告

热门文章

  1. 什么是认知偏见_偏见
  2. 蚂蚁笔记(Leanote)------一款国内优秀的开源项目
  3. 视频剪辑软件哪个好用?快把这些软件收好
  4. 字符函数库cctype的使用_C++
  5. Android 多点触控消息捕获与处理
  6. 怎么判断链表中是否有环
  7. suse 如何启动及配置sshd
  8. Web前端:JavaScript基础篇之var关键字
  9. MySQL自带的数据库界面化工具MySQL Workbench的安装
  10. java基础案例-购物车模拟