美文网首页Flutter中文社区flutter前端开发
Flutter 10天高仿大厂App及小技巧积累总结

Flutter 10天高仿大厂App及小技巧积累总结

作者: 南泽1 | 来源:发表于2020-05-25 13:31 被阅读0次

    以下博文会分为3个部分概述:

    项目结构分析

    项目功能详细概述(所用知识点)
    小技巧积累总结
    项目结构分析

    其次,梳理下项目的目录结构,理解每个文件都是干什么的,我们先来看看一级目录,如下:

    ├── README.md # 描述文件
    ├── android # android 宿主环境
    ├── build # 项目构建目录,由flutter自动完成
    ├── flutter_ctrip.iml
    ├── fonts # 自己创建的目录,用于存放字体
    ├── images # 自己创建的目录,用于存放图片
    ├── ios # iOS 宿主环境
    ├── lib # flutter 执行文件,自己写的代码都在这
    ├── pubspec.lock # 用来记录锁定插件版本
    ├── pubspec.yaml # 插件及资源配置文件
    └── test # 测试目录

    这个就不用多解释,大多是 flutter 生成及管理的,我们需要关注的是 lib 目录。

    我们再来看看二级目录,如下 (重点关注下lib目录)

    ├── README.md
    ├── android
    │ ├── android.iml
    ...
    │ └── settings.gradle
    ├── build
    │ ├── app
    ...
    │ └── snapshot_blob.bin.d.fingerprint
    ├── flutter_ctrip.iml
    ├── fonts
    │ ├── PingFang-Italic.ttf
    │ ├── PingFang-Regular.ttf
    │ └── PingFang_Bold.ttf
    ├── images
    │ ├── grid-nav-items-dingzhi.png
    ...
    │ └── yuyin.png
    ├── ios
    │ ├── Flutter
    ...
    │ └── ServiceDefinitions.json
    ├── lib
    │ ├── dao # 请求接口的类
    │ ├── main.dart # flutter 入口文件
    │ ├── model # 实体类,把服务器返回的 json 数据,转换成 dart 类
    │ ├── navigator # bottom bar 首页底部导航路由
    │ ├── pages # 所以的页面
    │ ├── plugin # 封装的插件
    │ ├── util # 工具类,避免重复代码,封装成工具类以便各个 page 调用
    │ └── widget # 封装的组件
    ├── pubspec.lock
    ├── pubspec.yaml
    └── test
    └── widget_test.dart

    再来看看,lib 目录下二级目录,看看整个项目创建了多少个文件,写了多少代码,如下 (其实,并不是很多)

    ├── dao/
    │ ├── destination_dao.dart*
    │ ├── destination_search_dao.dart*
    │ ├── home_dao.dart
    │ ├── search_dao.dart*
    │ ├── trave_hot_keyword_dao.dart*
    │ ├── trave_search_dao.dart*
    │ ├── trave_search_hot_dao.dart*
    │ ├── travel_dao.dart*
    │ ├── travel_params_dao.dart*
    │ └── travel_tab_dao.dart*
    ├── main.dart
    ├── model/
    │ ├── common_model.dart
    │ ├── config_model.dart
    │ ├── destination_model.dart
    │ ├── destination_search_model.dart
    │ ├── grid_nav_model.dart
    │ ├── home_model.dart
    │ ├── sales_box_model.dart
    │ ├── seach_model.dart*
    │ ├── travel_hot_keyword_model.dart
    │ ├── travel_model.dart*
    │ ├── travel_params_model.dart*
    │ ├── travel_search_hot_model.dart
    │ ├── travel_search_model.dart
    │ └── travel_tab_model.dart
    ├── navigator/
    │ └── tab_navigater.dart
    ├── pages/
    │ ├── destination_page.dart
    │ ├── destination_search_page.dart
    │ ├── home_page.dart
    │ ├── my_page.dart
    │ ├── search_page.dart
    │ ├── speak_page.dart*
    │ ├── test_page.dart
    │ ├── travel_page.dart
    │ ├── travel_search_page.dart
    │ └── travel_tab_page.dart*
    ├── plugin/
    │ ├── asr_manager.dart*
    │ ├── side_page_view.dart
    │ ├── square_swiper_pagination.dart
    │ └── vertical_tab_view.dart
    ├── util/
    │ └── navigator_util.dart*
    └── widget/
    ├── grid_nav.dart
    ├── grid_nav_new.dart
    ├── loading_container.dart
    ├── local_nav.dart
    ├── sales_box.dart
    ├── scalable_box.dart
    ├── search_bar.dart*
    ├── sub_nav.dart
    └── webview.dart

    整个项目就是以上这些文件了 (具体的就不一个一个分析了,如,感兴趣,大家可以 clone 源码运行起来,自然就清除了)。

    项目功能详细概述(所用知识点)

    首先,来看看首页功能及所用知识点,首页重点看下以下功能实现:

    • 渐隐渐现的 appBbar
    • 搜索组件的封装
    • 语音搜索页面
    • banner组件
    • 浮动的 icon 导航
    • 渐变不规则带有背景图的网格导航
    • 渐隐渐现的 appBbar

    先来看看具体效果,一睹芳容,如图:

    在这里插入图片描述

    滚动的时候 appBar 背景色从透明变成白色或白色变成透明,这里主要用了 flutter 的 NotificationListener 组件,它会去监听组件树冒泡事件,当被它包裹的的组件(子组件) 发生变化时,Notification 回调函数会被触发,所以,通过它可以去监听页面的滚动,来动态改变 appBar 的透明度(alpha),代码如下:

    NotificationListener(
      onNotification: (scrollNotification) {
        if (scrollNotification is ScrollUpdateNotification &&
            scrollNotification.depth == 0) {
          _onScroll(scrollNotification.metrics.pixels);
        }
        return true;
      },
      child: ...
    

    Tips:
    scrollNotification.depth 的值 0 表示其子组件(只监听子组件,不监听孙组件);
    scrollNotification is ScrollUpdateNotification 来判断组件是否已更新,ScrollUpdateNotification 是 notifications 的生命周期一种情况,分别有一下几种:

    ScrollStartNotification 组件开始滚动
    ScrollUpdateNotification 组件位置已经发生改变
    ScrollEndNotification 组件停止滚动
    UserScrollNotification 不清楚
    这里,我们不探究太深入,如想了解可多查看源码。

    _onScroll 方法代码如下:

    void _onScroll(offset) {
    double alpha = offset / APPBAR_SCROLL_OFFSET; // APPBAR_SCROLL_OFFSET 常量,值:100;offset 滚动的距离

    //把 alpha 值控制值 0-1 之间
    if (alpha < 0) {
      alpha = 0;
    } else if (alpha > 1) {
      alpha = 1;
    }
    setState(() {
      appBarAlpha = alpha;
    });
    print(alpha);
    

    }

    搜索组件的封装

    搜索组件效果如图:

    在这里插入图片描述

    以下是首页调用 searchBar 的代码:

    SearchBar(
      searchBarType: appBarAlpha > 0.2  //searchBar 的类:暗色、亮色
          ? SearchBarType.homeLight
          : SearchBarType.home,
      inputBoxClick: _jumpToSearch,     //点击回调函数
      defaultText: SEARCH_BAR_DEFAULT_TEXT,   // 提示文字
      leftButtonClick: () {},           //左边边按钮点击回调函数
      speakClick: _jumpToSpeak,         //点击话筒回调函数
      rightButtonClick: _jumpToUser,    //右边边按钮点击回调函数
    ),
    

    其实就是用 TextField 组件,再加一些样式,需要注意点是:onChanged,他是 TextField 用来监听文本框是否变化,通过它我们来监听用户输入,来请求接口数据;
    具体的实现细节,请查阅源码: 点击查看searchBar源码

    语音搜索页面

    语音搜索页面效果如图:由于模拟器无法录音,所以无法展示正常流程,如果录音识别成功后会返回搜索页面,在项目预览视频中可以看到正常流程。


    在这里插入图片描述

    no-shadow

    语音搜索功能使用的是百度的语言识别SDK,原生接入之后,通过 MethodChannel 和原生Native端通信,这里不做重点讲述(这里会涉及原生Native的知识)。

    重点看看点击录音按钮时的动画实现,这个动画用了 AnimatedWidget 实现的,代码如下:

    class AnimatedWear extends AnimatedWidget {
      final bool isStart;
      static final _opacityTween = Tween<double>(begin: 0.5, end: 0); // 设置透明度变化值
      static final _sizeTween = Tween<double>(begin: 90, end: 260);   // 设置圆形线的扩散值
    
      AnimatedWear({Key key, this.isStart, Animation<double> animation})
          : super(key: key, listenable: animation);
    
      @override
      Widget build(BuildContext context) {
        final Animation<double> animation = listenable;  // listenable 继承 AnimatedWidget,其实就是控制器,会自动监听组件的变化
        return Container(
          height: 90,
          width: 90,
          child: Stack(
            overflow: Overflow.visible,
            alignment: Alignment.center,
            children: <Widget>[
              ```
              // 扩散的圆线,其实就是用一个圆实现的,设置圆为透明,设置border
              Positioned(
                left: -((_sizeTween.evaluate(animation) - 90) / 2), // 根据 _sizeTween 动态设置left偏移值
                top: -((_sizeTween.evaluate(animation) - 90) / 2), //  根据 _sizeTween 动态设置top偏移值
                child: Opacity(
                  opacity: _opacityTween.evaluate(animation),      // 根据 _opacityTween 动态设置透明值
                  child: Container(
                    width: isStart ? _sizeTween.evaluate(animation) : 0, // 设置 宽
                    height: _sizeTween.evaluate(animation),              // 设置 高
                    decoration: BoxDecoration(
                        color: Colors.transparent,
                        borderRadius: BorderRadius.circular(
                            _sizeTween.evaluate(animation) / 2),
                        border: Border.all(
                          color: Color(0xa8000000),
                        )),
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }
    

    其他细节,如:点击时提示录音,录音失败提示,点击录音按钮出现半透明黑色圆边框,停止后消失等,请查看源码

    banner组件

    效果如图:


    aHR0cHM6Ly9zZWdtZW50ZmF1bHQuY29tL2ltZy9yZW1vdGUvMTQ2MDAwMDAyMjYxNjc2NQ.jpg

    banner使用的是flutter的 flutter_swiper 插件实现的,代码如下:

    Swiper(
      itemCount: bannerList.length,              // 滚动图片的数量
      autoplay: true,                            // 自动播放
      pagination: SwiperPagination(              // 指示器
          builder: SquareSwiperPagination(
            size: 6,                             // 指示器的大小
            activeSize: 6,                       // 激活状态指示器的大小
            color: Colors.white.withAlpha(80),   // 颜色
            activeColor: Colors.white,           // 激活状态的颜色
          ),
        alignment: Alignment.bottomRight,        // 对齐方式
        margin: EdgeInsets.fromLTRB(0, 0, 14, 28), // 边距
      ),
      itemBuilder: (BuildContext context, int index) { // 构造器
        return GestureDetector(
          onTap: () {
            CommonModel model = bannerList[index];
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => WebView(
                  url: model.url,
                ),
              ),
            );
          },
          child: Image.network(
            bannerList[index].icon,
            fit: BoxFit.fill,
          ),
        );
      },
    ),
    

    具体使用方法,可以去 flutter的官方插件库 pub.dev 查看:点击flutter_swiper查看。

    Tips:

    需要注意的是,我稍改造了一下指示器的样式,flutter_swiper 只提供了 3 种指示器样式,如下:

    dots = const DotSwiperPaginationBuilder(),圆形
    fraction = const FractionPaginationBuilder(),百分数类型的,如:1/6,表示6页的第一页
    rect = const RectSwiperPaginationBuilder(),矩形
    并没有上图的激活状态的长椭圆形,其实就是按葫芦画瓢,自己实现一个长椭圆类型,如知详情,可点击查看长椭圆形指示器源码

    浮动的 icon 导航

    icon导航效果如图:


    在这里插入图片描述

    iconBar

    icon导航浮动在banner之上,其实用的是 flutter 的 Stack 组件,Stack 组件能让其子组件堆叠显示,它通常和 Positioned 组件配合使用,布局结构代码如下:

    ListView(
      children: <Widget>[
        Container(
          child: Stack(
            children: <Widget>[
              Container( ... ), //这里放的是banner的代码
              Positioned( ... ), //这个就是icon导航,通过 Positioned 固定显示位置
            ],
          ),
        ),
        Container( ... ), // 这里放的网格导航及其他
      ],
    )
    

    2020大厂面试题
    点击进群交流 密码:111
    进群直接获取2020 BAT、阿里 大厂面试题,

    在这里插入图片描述
    有一个学习的气氛最重要

    渐变不规则带有背景图的网格导航

    网格导航效果如图:


    在这里插入图片描述

    如图,网格导航分为三行四栏,而第一行分为三栏,每一行的第一栏宽度大于其余三栏,其余三栏均等,每一行都有渐变色,而且第一、二栏都有背景图;
    flutter 里 Column 组件能让子组件竖轴排列, Row 组件能让子组件横轴排列,布局代码如下:

    Column(                      // 最外面放在 Column 组件
      children: <Widget>[
        Container(               // 第一行包裹 Container 设置其渐变色
          height: 72,
          decoration: BoxDecoration(
            gradient: LinearGradient(colors: [  //设置渐变色
              Color(0xfffa5956),
              Color(0xffef9c76).withAlpha(45)
            ]),
          ),
          child: Row( ... ),    // 第一行
        ),
        Padding(
          padding: EdgeInsets.only(top: 1),  // 设置行直接的间隔
        ),
        Container(
          height: 72,
          decoration: BoxDecoration(
            gradient: LinearGradient(colors: [  //设置渐变色
              Color(0xff4b8fed),
              Color(0xff53bced),
            ]),
          ),
          child: Row( ... ),  // 第二行
        ),
        Padding(
          padding: EdgeInsets.only(top: 1),   // 设置行直接的间隔
        ),
        Container(
          height: 72,
          decoration: BoxDecoration(
            gradient: LinearGradient(colors: [  //设置渐变色
              Color(0xff34c2aa),
              Color(0xff6cd557),
            ]),
          ),
          child: Row( ... ),  // 第三行
        ),
      ],
    ),
    

    其实,具体实现的细节还是很多的,比如:

    • 怎么设置第一栏宽度偏大,其他均等;

    • 第一行最后一栏宽度是其他的2倍;

    • 第一、二栏的别截图及浮动的红色气泡tip等;

    • 在这里就不细讲,否则篇幅太长,如想了解详情 点击查看源码

    • 其次,再来看看目的地页面功能及所用知识点,重点看下以下功能实现:

    • 左右布局tabBarListView

    • 目的地搜索页面

    左右布局tabBarListView

    具体效果如图:点击左边标签可以切换页面,左右滑动也可切换页面,点击展开显示更多等


    在这里插入图片描述

    其实官方已经提供了 tabBar 和 TabBarView 组件可以实现上下布局的效果(旅拍页面就是用这个实现的),但是它无法实现左右布局,而且不太灵活,所以,我使用的是 vertical_tabs插件, 代码如下:

    VerticalTabView(
        tabsWidth: 88,
        tabsElevation: 0,
        indicatorWidth: 0,
        selectedTabBackgroundColor: Colors.white,
        backgroundColor: Colors.white,
        tabTextStyle: TextStyle(
          height: 60,
          color: Color(0xff333333),
        ),
        tabs: tabs,
        contents: tabPages,
      ),
    ),
    

    具体使用方法,在这里就不赘述了,点击vertical_tabs查看

    Tips:
    这里需要注意的是:展开显示更多span标签组件的实现,因为,这个组件在很多的其他组件里用到而且要根据接口数据动态渲染,且组件自身存在状态的变化,这种情况下,最好是把他单独封装成一个组件(widget),否则,很难控制自身状态的变化,出现点击没有效果,或点击影响其他组件。

    目的地搜索页面

    效果如图:点击搜索结果,如:点击‘一日游‘,会搜索到‘一日游‘的相关数据


    在这里插入图片描述

    目的地搜索页面,大多都是和布局和对接接口的代码,在这里就不再赘述。

    然后就是旅拍页面功能及所用知识点,重点看下以下功能实现:

    左右布局tabBarListView

    瀑布流卡片

    旅拍搜索页

    左右布局tabBarListView

    效果如图:可左右滑动切换页面,上拉加载更多,下拉刷新等

    no-shadow

    这个是flutter 提供的组件,tabBar 和 TabBarView,代码如下:

    Container(
      color: Colors.white,
      padding: EdgeInsets.only(left: 2),
      child: TabBar(
        controller: _controller,
        isScrollable: true,
        labelColor: Colors.black,
        labelPadding: EdgeInsets.fromLTRB(8, 6, 8, 0),
        indicatorColor: Color(0xff2FCFBB),
        indicatorPadding: EdgeInsets.all(6),
        indicatorSize: TabBarIndicatorSize.label,
        indicatorWeight: 2.2,
        labelStyle: TextStyle(fontSize: 18),
        unselectedLabelStyle: TextStyle(fontSize: 15),
        tabs: tabs.map<Tab>((Groups tab) {
          return Tab(
            text: tab.name,
          );
        }).toList(),
      ),
    ),
    Flexible(
        child: Container(
      padding: EdgeInsets.fromLTRB(6, 3, 6, 0),
      child: TabBarView(
          controller: _controller,
          children: tabs.map((Groups tab) {
            return TravelTabPage(
              travelUrl: travelParamsModel?.url,
              params: travelParamsModel?.params,
              groupChannelCode: tab?.code,
            );
          }).toList()),
    )),
    ### 瀑布流卡片
    
    瀑布流卡片 用的是 flutter_staggered_grid_view 插件,代码如下:
    
    StaggeredGridView.countBuilder(
      controller: _scrollController,
      crossAxisCount: 4,
      itemCount: travelItems?.length ?? 0,
      itemBuilder: (BuildContext context, int index) => _TravelItem(
            index: index,
            item: travelItems[index],
          ),
      staggeredTileBuilder: (int index) => new StaggeredTile.fit(2),
      mainAxisSpacing: 2.0,
      crossAxisSpacing: 2.0,
    ),
    

    如下了解更多相关信息,点击flutter_staggered_grid_view查看。

    旅拍搜索页

    效果如图:首先显示热门旅拍标签,点击可搜索相关内容,输入关键字可搜索相关旅拍信息,地点、景点、用户等


    在这里插入图片描述
    旅拍搜索页,大多也是和布局和对接接口的代码,在这里就不再赘述。

    小技巧积累总结

    以下都是我在项目里使用的知识点,在这里记录分享出来,希望能帮到大家。

    PhysicalModel

    PhysicalModel 可以裁剪带背景图的容器,如,你在一个 Container 里放了一张图片,想设置图片圆角,设置 Container 的 decoration 的 borderRadius 是无效的,这时候就要用到 PhysicalModel,代码如下:

    PhysicalModel(
      borderRadius: BorderRadius.circular(6),  // 设置圆角
      clipBehavior: Clip.antiAlias,            // 裁剪行为
      color: Colors.transparent,               // 颜色
      elevation: 5,                            // 设置阴影
      child: Container(
            child: Image.network(
              picUrl,
              fit: BoxFit.cover,
            ),
          ),
    ),
    LinearGradient
    

    给容器添加渐变色,在网格导航、appBar等地方都使用到,代码如下:

    Container(
      height: 72,
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [
          Color(0xff4b8fed),
          Color(0xff53bced),
        ]),
      ),
      child: ...
    ),
    Color(int.parse('0xff' + gridNavItem.startColor))
    

    颜色值转换成颜色,如果,没有变量的话,也可直接这样用 Color(0xff53bced),

    ox:flutter要求,可固定不变
    ff:代表透明贴,不知道如何设置的话,可以用取色器,或者 withOpacity(opacity) 、 withAlpha(a)
    53bced: 常规的6位RGB值
    Expanded、FractionallySizedBox

    Expanded 可以让子组件撑满父容器,通常和 Row 及 Column 组件搭配使用;

    FractionallySizedBox 可以让子组件撑满或超出父容器,可以单独使用,大小受 widthFactor 和 heightFactor 宽高因子的影响

    MediaQuery.removePadding

    MediaQuery.removePadding 可以移除组件的边距,有些组件自带有边距,有时候布局的时候,不需要边距,这时候就可以用 MediaQuery.removePadding,代码如下:

    MediaQuery.removePadding(
      removeTop: true,
      context: context,
      child: ...
    )
    MediaQuery.of(context).size.width
    

    MediaQuery.of(context).size.width 获取屏幕的宽度,同理,MediaQuery.of(context).size.height 获取屏幕的高度;
    如,想一行平均3等分: 0.3 * MediaQuery.of(context).size.width,在目的地页面的标签组件就使用到它,代码如下:

    Container(
      alignment: Alignment.center,
      ...
      width: 0.3*MediaQuery.of(context).size.width - 12, // 屏幕平分三等分, - 12 是给每份中间留出空间 
      height: 40,
      ...
      child: ...
    ),
    Theme.of(context).platform == TargetPlatform.iOS
    

    判断操作系统类型,有时候可能有给 Andorid 和 iOS 做出不同的布局,就需要用到它。

    with AutomaticKeepAliveClientMixin

    flutter 在切换页面时候每次都会重新加载数据,如果想让页面保留状态,不重新加载,就需要使用 AutomaticKeepAliveClientMixin,代码如下:(在旅拍页面就有使用到它,为了让tabBar 和 tabBarView在切换时不重新加载)

    class TravelTabPage extends StatefulWidget {
    ...
    //需要重写 wantKeepAlive 且 设置成 true
    @override
    bool get wantKeepAlive => true;
    }
    暂时只能想到这些常用的知识点,以后如有新的会慢慢补充。

    推荐

    2020大厂面试题
    点击进群交流 密码:111
    进群直接获取2020 BAT、阿里 大厂面试题,

    在这里插入图片描述
    有一个学习的氛围最重要

    相关文章

      网友评论

        本文标题:Flutter 10天高仿大厂App及小技巧积累总结

        本文链接:https://www.haomeiwen.com/subject/aklyahtx.html