Flutter项目实战之女装商城------添加轮播图、分类导航、商品推荐、广告位处理、底部商品

前言:

时间飞逝,伴随着打工人春节的结束心也得跟着回收一下,新年新气象,今年也没有什么高大上的目标,暂且先把去年落下的各种课程的学习给完整结束了,不过计划还是会再多学点其它新的东东,到时看情况,年后的第一次学习从之前落下的Flutter开始。

距离上一次Flutter项目实战之女装商城------首页设计分析、数据准备、Dio请求处理、接口配置、请求首页数据 - cexo - 博客园的学习已经时隔4个月之久了,先来回忆一下当时APP的样子做到了哪了:

其实要注意,后台数据是用的node来搭建的,注意本地先npm start一下:

然后本机ip会随时变动,记得根据本机当前的ip,将代码修改一下,比如:

基本也就搭了个架子,接下来则从首页开始继续往下撸。

添加轮播图:

效果:

接下来先来搭建一下首页的商品轮播图,样子如下:

添加依赖:

要实现这样的效果,当然得借助于三方库喽,这里使用flutter_swiper,官方地址:flutter_swiper | Flutter Package

先来在工程中添加依赖:

构建轮播组件:

1、导包:

先来对home_page的代码进行一个全局回顾,目前长这样子:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:fluttershop/config/color.dart';
import 'package:fluttershop/config/string.dart';
import 'package:fluttershop/service/http_service.dart';
import 'package:flutter_swiper/flutter_swiper.dart';

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with AutomaticKeepAliveClientMixin {
  //这里只是做一个演示,其实是不用加这个的,因为咱们的BottomNavigationBar是由IndexedStack构建的,具体参考index_page.dart文件
  @override
  bool get wantKeepAlive => true;
  GlobalKey<ClassicalFooterWidgetState> _footerKey =
      GlobalKey<ClassicalFooterWidgetState>();

  @override
  void initState() {
    super.initState();
    print('home_page.initState()');
  }

  @override
  Widget build(BuildContext context) {
    print('home_page.build()');
    return Scaffold(
      backgroundColor: Color.fromRGBO(244, 245, 245, 1.0),
      appBar: AppBar(
        title: Text(KString.homeTitle),
        centerTitle: true,
      ),
      body: FutureBuilder(
          future: request('homePageContext', formData: null),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              var data = json.decode(snapshot.data.toString());
              print("gethomePageContext data:${data}");
              var dataList = data['data'];
              List<Map> swiperDataList =
                  (dataList['slides'] as List).cast(); //轮播图
              List<Map> navigatorList =
                  (dataList['category'] as List).cast(); //分类
              List<Map> recommendList =
                  (dataList['recommend'] as List).cast(); //商品推荐
              List<Map> floor1 = (dataList['floor1'] as List).cast(); //底部商品推荐
              Map fp1 = dataList['floor1Pic']; //广告
              return EasyRefresh.custom(
                enableControlFinishRefresh: false,
                enableControlFinishLoad: true,
                footer: ClassicalFooter(
                  key: _footerKey,
                  bgColor: Colors.white,
                  textColor: KColor.refreshTextColor,
                  infoColor: KColor.refreshTextColor,
                  noMoreText: '',
                  //加载中...
                  loadingText: KString.loading,
                  loadReadyText: KString.loadReadyText,
                ),
                slivers: [
                  SliverList(
                    delegate: SliverChildBuilderDelegate(
                      (context, index) {
                        return Container(
                          width: 60.0,
                          height: 60.0,
                          child: Center(
                            child: Text('$index'),
                          ),
                          color: index % 2 == 0
                              ? Colors.grey[300]
                              : Colors.transparent,
                        );
                      },
                      childCount: 5,
                    ),
                  ),
                ],
                onLoad: () async {
                  print('开始加载更多');
                  //TODO 执行下一页的请求
                },
              );
            } else {
              return Center(
                child: Text('加载中...'),
              );
            }
          }),
    );
  }
}

此时需要用ListView来构建整个界面的内容,所以代码需要修改一下:

2、编写Swiper组件框架:

其中注意Dart的语法,我在编写这块时都感觉完全生疏了,时间一长工作中又不用铁定健忘,这时之前学习留下的笔记就发挥它的威力了,这里说的Dart语法就是构造函数相关,一个是:

关于它可以回忆Fluter基础巩固之Dart语言详解<二> - cexo - 博客园

另一个是在它定义构造时用了一个大括号:

这又是啥语法呢?可选命名参数,如果忘了可以参考Fluter基础巩固之Dart语言详解<一> - cexo - 博客园,如下:

3、具体编写Swiper组件:

接下来具体编写Swpier组件的构建,先来参考官方的DEMO:

这里就一气呵成了,只对关键地方做下说明:

其中关于fit属性有很多种,这里参考大佬的Flutter中 BoxFit 的几种样式说明,贴出来为我所用:

BoxFit.contain

将会尽可能的伸展来达到组件的边缘。

Flutter中 BoxFit 的几种样式

BoxFit.cover

将会尽可能小的放大来铺满整个组件

Flutter中 BoxFit 的几种样式

BoxFit.fill

通过拉伸(改变原图比例)来铺满组件

Flutter中 BoxFit 的几种样式

BoxFit.fitHeight

保持原图比例,拉升高度来达到组件的高度

Flutter中 BoxFit 的几种样式

BoxFit.fitWidth

保持原图比例,拉升宽度来达到组件的宽度

Flutter中 BoxFit 的几种样式

BoxFit.none

正常展示,默认居中。

Flutter中 BoxFit 的几种样式

BoxFit.scaleDown

保持在组件中并且不改变比例

Flutter中 BoxFit 的几种样式

另外还有一个关于设置尺寸的代码:

这块是屏幕适配的写法,记得不要直接写死一个值,关于这块可以回忆博文阅读密码验证 - 博客园,不过多解释。

然后需要设置里面小红点和自动轮播,很简单:

4、运行:

呃,咋图片没加载出来空白了呢?这里要注意了【以后这块就不强调了】!!!然后看一下每个图片的url是这么个形式:

因为node服务是运行在电脑上的,127.0.0.1代表的是电脑的本机ip,但是!!!我运行是在真机上,而非电脑上开设的模拟器,那很显然在手机上是无法通过127.0.0.1这个电脑端的本地地址来访问图片了,解决办法也很简单,直接用电脑的局域网ip既可,查看一下电脑的ip:

然后看一下目前咱们node的baseurl的ip是:

当然这个ip可能随着网络的变化会不断变化,所以以后在学习之前一定先要确保ip地址是正确的,再重启一下npm,然后再运行就ok了:

效果如下:

嗯,不错~~整体也比较简单。

分类导航:

效果:

其中图标都是后台返的,另外最多就支持10个分类,如果超过10个则需要截取。

具体实现:

1、列表中增加一个widget:

2、使用GridView来构建:

对于分类这种网络布局可以使用Flutter自带的GridView来进行构建,关于它的用法可以参考开启Fluter基础之旅<二>-------Future再论、常用组件、Material Design风格组件学习 - cexo - 博客园,下面来构建一下:

这块比较简单,其中标红的在设置大小时都用到了EdgeInsets这个类,这个在之前的学习中也大量用到了,看一下这个类的官方解释:

总之就是对于方便给一个元素设置左右上下的间隙,而它比较经典的方法有Flutter之EdgeInsets_u013095264的博客-CSDN博客

关于这块要记得熟练运用,另外对于GridView的这个属性也稍加说明一下:

其中physics是Flutter中的动画的一个种类,回忆一下开启Fluter基础之旅<四>-------表格、动画、手势 - cexo - 博客园

而常见的physics有很多,看一下它的子类便可以了解:

这块就点一下,可以在之后慢慢了解。

接下来则来设置一下GridView的item项:

那如何构建呢?如下:

接下来就来构建一下列表项,由于它也是需要可以进行点击的,所以依然用InkWell,比较简单,贴出来:

Widget _gridViewItemUI(BuildContext context, item, index) {
    return InkWell(
      onTap: () {
        //TODO 商品分类点击进详情
      },
      child: Column(
        children: [
          Image.network(
            item['image'],
            width: ScreenUtil().setWidth(95),
          ),
          Text(item['firstCategoryName']),
        ],
      ),
    );
  }

接下来运行一下,看有木有问题,发现报错了。。

看IDE报错的详情:

其实是犯了一个低级错,啥错呢?

再运行一下:

商品推荐:

效果:

 当然是支持左右滑动了。

具体实现:

1、列表中增加一个widget:

//商品推荐组件
class RecommendUI extends StatelessWidget {
  final List<Map> recommendList;

  RecommendUI({Key key, this.recommendList}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return null;
  }
}

2、准备推荐商品标题widget:

先来准备子widget,布局也就是按从局部到整体的思路一步步来进行,也就是推荐的标题,如下:

其中涉及到几个色值和字符串,提到公共的地方为:

3、准备推荐商品列表widget:

目前先简单显示一个文本,先将整个界面架子组拼起来。

4、将子widget组拼起来:

接下来整体看一下基本架子的样子:

5、准备列表item的widget:

//列表item
  Widget _item(context, index) {
    return InkWell(
      onTap: () {
        //TODO 商品点击进详情
      },
      child: Container(
        width: ScreenUtil().setWidth(200),
        padding: EdgeInsets.all(0.0),
        decoration: BoxDecoration(
            color: Colors.white,
            border: Border(
              left: BorderSide(width: 0.5, color: KColor.defaultBorderColor),
            )),
        child: Column(
          children: [
            Image.network(recommendList[index]['image']),
            //打折价格
            Text(
              '¥${recommendList[index]['presentPrice']}',
              style: TextStyle(color: KColor.presentPriceTextColor),
            ),
            //原价
            Text(
              '¥${recommendList[index]['oriPrice']}',
              style: KFont.oriPriceStyle,
            ),
          ],
        ),
      ),
    );

其中配置相关的属性如下:

其中对于有中划线的原价以后在多处页面都可能会用到,所以这里对这样的样式字体进行一个抽取:

接下来运行看一下效果:

细节调整:

细节一:防止溢出处理

看这张图:

其实这里加一个防溢出的代码既可,如下:

再运行:

看一下它的作用:

细节二:图片压缩调整

目前对于这张图很不和谐:

此时就需要给图片增加压缩属性:

再看效果:

貌似上下木有间隙,看着有点别扭,因为有处笔误写错了:

修改一下:

再来看一下:

关于Image的几种压缩效果自己一个个试就成,不需要记,做得多了也就知道其它的意思了,就类似于Android的ImageView控件一样,它的缩放方式也很多,实际中还得根据产品的需求自己去尝试调试,具体这块的属性可以参考:Flutter中Image的fit属性解析 - 简书

广告位处理:

效果:

就是一张纯图,没啥逻辑。

具体实现: 

1、准备商品广告widget:

这块比较简单,直接贴出:

//商品推荐中间广告
class FloorPic extends StatelessWidget {
  final Map floorPic;

  FloorPic({Key key, this.floorPic}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
        margin: EdgeInsets.only(top: 10.0),
        child: InkWell(
          onTap: () {
            //TODO 商品点击进详情
          },
          child: Image.network(
            floorPic['PICTURE_ADDRESS'],
            fit: BoxFit.cover,
          ),
        ));
  }
}

2、运行:

底部商品:

效果:

貌似这布局有点复杂,其实这里是涉及到row和column的嵌套布局,也是实际项目中会经常用到的一种布局方式,通过这个模块的编写正好可以掌握这种布局方法。

具体实现:

1、列表中增加一个widget:

//商品推荐下层
class Floor extends StatelessWidget {
  final List<Map> floor;

  Floor({Key key, this.floor}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

2、使用Row来搭建左右两列效果:

3、具体构建五大子widget:

先来构建左边上面大图:

比较简单,接下来四个就依葫芦画瓢了,当然间隙需要稍加修改一下,一口气呵成:

//商品推荐下层
class Floor extends StatelessWidget {
  final List<Map> floor;

  Floor({Key key, this.floor}) : super(key: key);

  //跳转到商品详情
  void jumpDetail(context, String goodId) {
    //TODO
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Row(
        children: [
          //左侧商品
          Expanded(
            child: Column(
              children: [
                //左上图
                Container(
                  padding: EdgeInsets.only(top: 4),
                  height: ScreenUtil().setHeight(400),
                  child: InkWell(
                      onTap: () {
                        jumpDetail(context, floor[0]['goodsId']);
                      },
                      child: Image.network(
                        floor[0]['image'],
                        fit: BoxFit.cover,
                      )),
                ),
                //左下图
                Container(
                  padding: EdgeInsets.only(top: 1, right: 1),
                  height: ScreenUtil().setHeight(200),
                  child: InkWell(
                      onTap: () {
                        jumpDetail(context, floor[1]['goodsId']);
                      },
                      child: Image.network(
                        floor[1]['image'],
                        fit: BoxFit.cover,
                      )),
                ),
              ],
            ),
          ),
          //右侧商品
          Expanded(
            child: Column(
              children: [
                //右上图
                Container(
                  padding: EdgeInsets.only(top: 4, left: 1, bottom: 1),
                  height: ScreenUtil().setHeight(200),
                  child: InkWell(
                      onTap: () {
                        jumpDetail(context, floor[2]['goodsId']);
                      },
                      child: Image.network(
                        floor[2]['image'],
                        fit: BoxFit.cover,
                      )),
                ),
                //右中图
                Container(
                  padding: EdgeInsets.only(top: 1, left: 1),
                  height: ScreenUtil().setHeight(200),
                  child: InkWell(
                      onTap: () {
                        jumpDetail(context, floor[3]['goodsId']);
                      },
                      child: Image.network(
                        floor[3]['image'],
                        fit: BoxFit.cover,
                      )),
                ),
                //右下图
                Container(
                  padding: EdgeInsets.only(top: 1, left: 1),
                  height: ScreenUtil().setHeight(200),
                  child: InkWell(
                      onTap: () {
                        jumpDetail(context, floor[4]['goodsId']);
                      },
                      child: Image.network(
                        floor[4]['image'],
                        fit: BoxFit.cover,
                      )),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

4、给Row设置属性:

最后还需要给Row设置两个属性,如下:

mainAxisAlignment:

其中它有很多选项:

其中只需要对spaceBetween、spaceAround、spaceEvenly进行了解既可,具体啥意思可以参考这位大佬的Flutter学习之MainAxisAlignment属性详解_bkysdc的博客-CSDN博客,介绍还是蛮详细的,其中盗个图:

三者文字简述如下:

  • spaceBetween:

  • spaceAround:

  • spaceEvenly:

对于这些属性的理解还得在以后项目中的使用中来慢慢消化。

mainAxisSize:

其中它也有两个选项:

啥意思呢?目前还是找度娘线性布局Row和Column - 小图教父 - 博客园【关于这些涉及到木有接触过的知识点以后有时间整体单独成篇再细致的研究一下,目前先直接网上一大抄来解决疑惑】,如下:

运行:

最后运行看一下效果:

那ios上运行效果是否也一样呢?这里开一个ios模拟器瞅一下:

嗯,还不错,只不过我电脑有点卡,录屏更卡,所以可以自行真机运行看一下性能怎么样~~

总结:

最后纵观一下对于这次首页功能的编写,发现整体的编写思路都是从局部的widget,再到全局的widget,一点点进行构建,比如拿商品推荐组件来说,它由标题和列表组成,于是乎将整体拆成了2个widget来进行实现:

这样实现起来代码可读性也比较强,而不要将所有的实现全塞到这块:

先学到这,下次继续~~

关注个人公众号,获得实时推送

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,我可以为您讲解一下Flutter自定义Tab导航的实现方法。 对于顶部导航,可以使用TabBar和TabBarView来实现。TabBar是一个水平的导航栏,TabBarView是一个可以滚动的控件,可以用来展示不同的页面内容。下面是一个简单的示例代码: ``` TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('顶部导航'), bottom: TabBar( controller: _tabController, tabs: [ Tab(text: 'Tab1'), Tab(text: 'Tab2'), ], ), ), body: TabBarView( controller: _tabController, children: [ // Tab1页面内容 Container( child: Text('Tab1'), ), // Tab2页面内容 Container( child: Text('Tab2'), ), ], ), ); } ``` 对于底部导航,可以使用BottomNavigationBar来实现。BottomNavigationBar是一个底部导航栏,可以用来切换不同的页面。下面是一个简单的示例代码: ``` int _currentIndex = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('底部导航'), ), body: Center( child: Text('当前页面: $_currentIndex'), ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { setState(() { _currentIndex = index; }); }, items: [ BottomNavigationBarItem( icon: Icon(Icons.home), title: Text('首页'), ), BottomNavigationBarItem( icon: Icon(Icons.search), title: Text('搜索'), ), BottomNavigationBarItem( icon: Icon(Icons.person), title: Text('个人中心'), ), ], ), ); } ``` 对于自定义Tab导航,可以使用自定义控件来实现。比如,可以使用Row和GestureDetector来构建一个自定义的Tab导航栏。下面是一个简单的示例代码: ``` int _currentIndex = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('自定义Tab导航'), ), body: Center( child: Text('当前页面: $_currentIndex'), ), bottomNavigationBar: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ GestureDetector( onTap: () { setState(() { _currentIndex = 0; }); }, child: Column( children: [ Icon(Icons.home, color: _currentIndex == 0 ? Colors.blue : Colors.grey), Text('首页', style: TextStyle(color: _currentIndex == 0 ? Colors.blue : Colors.grey)), ], ), ), GestureDetector( onTap: () { setState(() { _currentIndex = 1; }); }, child: Column( children: [ Icon(Icons.search, color: _currentIndex == 1 ? Colors.blue : Colors.grey), Text('搜索', style: TextStyle(color: _currentIndex == 1 ? Colors.blue : Colors.grey)), ], ), ), GestureDetector( onTap: () { setState(() { _currentIndex = 2; }); }, child: Column( children: [ Icon(Icons.person, color: _currentIndex == 2 ? Colors.blue : Colors.grey), Text('个人中心', style: TextStyle(color: _currentIndex == 2 ? Colors.blue : Colors.grey)), ], ), ), ], ), ); } ``` 以上是三种常见的Tab导航实现方法,您可以根据自己的需求选择合适的方式来实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

webor2006

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值