以前,也写过几篇关于 Flutter
的博文,最近,又花了一些时间学习研究 Flutter
,完成了高仿大厂 App 项目 (项目使用的接口都是来自线上真实App抓包而来,能够作到和线上项目相同的效果),也总结积累了一些小技巧和知识点,因此,在这里记录分享出来,也但愿 Flutter
生态愈来愈好 (flutter开发App效率真的很高,开发体验也是很好的 🙂)。android
如下博文会分为4个部分概述:ios
首先,咱们来经过一个视频来快速预览下项目完成的功能和运行效果,以下git
你们看完视频,大概了解到,完成度基本能够和线上的App相差无异了,你们若是对项目感兴趣,想了解具体怎么实现的,能够去个人 GitHub clone 源码查看。github
本视频是用真机录屏的,由于,语音搜索功能须要录音,模拟器没法录音,固然, iOS
和 Andorid
均可以运行,效果是同样的,如图:web
其次,梳理下项目的目录结构,理解每一个文件都是干什么的,咱们先来看看一级目录,以下:json
├── README.md # 描述文件
├── android # android 宿主环境
├── build # 项目构建目录,由flutter自动完成
├── flutter_ctrip.iml
├── fonts # 本身建立的目录,用于存放字体
├── images # 本身建立的目录,用于存放图片
├── ios # iOS 宿主环境
├── lib # flutter 执行文件,本身写的代码都在这
├── pubspec.lock # 用来记录锁定插件版本
├── pubspec.yaml # 插件及资源配置文件
└── test # 测试目录
复制代码
这个就不用多解释,大可能是 flutter 生成及管理的,咱们须要关注的是 lib 目录。bash
咱们再来看看二级目录,以下 (重点关注下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 目录下二级目录,看看整个项目建立了多少个文件,写了多少代码,以下 (其实,并非不少)app
├── 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 源码运行起来,天然就清除了)。ide
首先,来看看首页功能及所用知识点,首页重点看下如下功能实现:
先来看看具体效果,一睹芳容,如图:
滚动的时候 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 的生命周期一种状况,分别有一下几种:
这里,咱们不探究太深刻,如想了解可多查看源码。
_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源码
语音搜索页面效果如图:因为模拟器没法录音,因此没法展现正常流程,若是录音识别成功后会返回搜索页面,在项目预览视频中能够看到正常流程。
语音搜索功能使用的是百度的语言识别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
使用的是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 种指示器样式,以下:
并无上图的激活状态的长椭圆形,其实就是按葫芦画瓢,本身实现一个长椭圆类型,如知详情,可点击查看长椭圆形指示器源码
icon导航效果如图:
icon导航浮动在banner之上,其实用的是 flutter
的 Stack 组件,Stack 组件能让其子组件堆叠显示,它一般和 Positioned 组件配合使用,布局结构代码以下:
ListView(
children: <Widget>[
Container(
child: Stack(
children: <Widget>[
Container( ... ), //这里放的是banner的代码
Positioned( ... ), //这个就是icon导航,经过 Positioned 固定显示位置
],
),
),
Container( ... ), // 这里放的网格导航及其余
],
),
复制代码
网格导航效果如图:
如图,网格导航分为三行四栏,而第一行分为三栏,每一行的第一栏宽度大于其他三栏,其他三栏均等,每一行都有渐变色,并且第1、二栏都有背景图; 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( ... ), // 第三行
),
],
),
复制代码
其实,具体实现的细节仍是不少的,好比:
在这里就不细讲,不然篇幅太长,如想了解详情 点击查看源码
其次,再来看看目的地页面功能及所用知识点,重点看下如下功能实现:
具体效果如图:点击左边标签能够切换页面,左右滑动也可切换页面,点击展开显示更多等
其实官方已经提供了 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查看
这里须要注意的是:展开显示更多span标签组件的实现,由于,这个组件在不少的其余组件里用到并且要根据接口数据动态渲染,且组件自身存在状态的变化,这种状况下,最好是把他单独封装成一个组件(widget),不然,很难控制自身状态的变化,出现点击没有效果,或点击影响其余组件。
效果如图:点击搜索结果,如:点击‘一日游‘,会搜索到‘一日游‘的相关数据
目的地搜索页面,大多都是和布局和对接接口的代码,在这里就再也不赘述。
而后就是旅拍页面功能及所用知识点,重点看下如下功能实现:
效果如图:可左右滑动切换页面,上拉加载更多,下拉刷新等
这个是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 能够裁剪带背景图的容器,如,你在一个 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,
),
),
),
复制代码
给容器添加渐变色,在网格导航、appBar等地方都使用到,代码以下:
Container(
height: 72,
decoration: BoxDecoration(
gradient: LinearGradient(colors: [
Color(0xff4b8fed),
Color(0xff53bced),
]),
),
child: ...
),
复制代码
颜色值转换成颜色,若是,没有变量的话,也可直接这样用 Color(0xff53bced)
,
Expanded 可让子组件撑满父容器,一般和 Row 及 Column 组件搭配使用;
FractionallySizedBox 可让子组件撑满或超出父容器,能够单独使用,大小受 widthFactor 和 heightFactor 宽高因子的影响
MediaQuery.removePadding 能够移除组件的边距,有些组件自带有边距,有时候布局的时候,不须要边距,这时候就能够用 MediaQuery.removePadding,代码以下:
MediaQuery.removePadding(
removeTop: true,
context: context,
child: ...
)
复制代码
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: ...
),
复制代码
判断操做系统类型,有时候可能有给 Andorid 和 iOS 作出不一样的布局,就须要用到它。
flutter
在切换页面时候每次都会从新加载数据,若是想让页面保留状态,不从新加载,就须要使用 AutomaticKeepAliveClientMixin,代码以下:(在旅拍页面就有使用到它,为了让tabBar 和 tabBarView在切换时不从新加载)
class TravelTabPage extends StatefulWidget {
...
//须要重写 wantKeepAlive 且 设置成 true
@override
bool get wantKeepAlive => true;
}
复制代码
暂时只能想到这些经常使用的知识点,之后若有新的会慢慢补充。
博客地址: lishaoy.net
博客Notes地址: h.lishaoy.net
项目GitHub地址: github.com/persilee/fl…