前一段時(shí)間項(xiàng)目集成了
Flutter
做了許多的功能模塊篇梭,再加上很久沒有文章產(chǎn)出兄渺,所以打算寫這么一篇文章來總結(jié)和記錄Flutter
開發(fā)中的一些問題
Demo地址:https://github.com/weibindev/flutter_order
ps
:demo中的數(shù)據(jù)都從assets\data\
文件夾下的json文件讀取,所以并沒有涉及到網(wǎng)絡(luò)請求封裝,項(xiàng)目架構(gòu)等相關(guān)知識,這個(gè)demo偏注重于點(diǎn)單結(jié)構(gòu)的實(shí)現(xiàn)沿侈。
總體的效果如下所示:
整體結(jié)構(gòu)分析
首頁的店鋪入口沒什么好說的,它主要是我們點(diǎn)單功能的入口和店鋪購物車商品數(shù)的展示市栗。
下面我們主要來分析下點(diǎn)單界面的結(jié)構(gòu)組成。
根據(jù)上面這張圖咳短,按照數(shù)字標(biāo)識框出的地方分析如下:
- 1:頂部的搜索框填帽,相當(dāng)于
Android
中的statusBar
+toolbar
- 2:左側(cè)一級商品分類欄目,部分欄目會有二級分類的情況出現(xiàn)
- 3:二級商品分類欄目咙好,是對一個(gè)大類商品做進(jìn)一步劃分
- 4:一級或二級分類的商品列表篡腌,點(diǎn)擊單個(gè)商品條目進(jìn)入商品的詳情頁
- 5:底部購物車,它位于整個(gè)點(diǎn)單界面的最頂層勾效,這個(gè)界面的所有功能均不會遮擋住購物車(具有
overlays
屬性的控件除外)
其中1嘹悼,2,3层宫,4可以看作一個(gè)整體杨伙,5可以看作一個(gè)整體。
底部購物車實(shí)現(xiàn)
關(guān)于底部購物車萌腿,我剛開始的實(shí)現(xiàn)思路是用Overlay
去做限匣,源碼中對它的描述如下
/// A [Stack] of entries that can be managed independently.
///
/// Overlays let independent child widgets "float" visual elements on top of
/// other widgets by inserting them into the overlay's [Stack]. The overlay lets
/// each of these widgets manage their participation in the overlay using
/// [OverlayEntry] objects.
///
/// Although you can create an [Overlay] directly, it's most common to use the
/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
/// navigator uses its overlay to manage the visual appearance of its routes.
///
/// See also:
///
/// * [OverlayEntry].
/// * [OverlayState].
/// * [WidgetsApp].
/// * [MaterialApp].
class Overlay extends StatefulWidget {
意思是Overlay
是一個(gè)Stack
組件,可以將OverlayEntry
插入到Overlay
中毁菱,使其獨(dú)立的child
窗口懸浮于其它組件之上米死,利用這個(gè)特性我們可以用Overlay
將底部購物車組件包裹起來,覆蓋在其它的組件之上贮庞。
然而實(shí)際使用過程中問題多多峦筒,需要自己精準(zhǔn)的控制好Overlay
包裹的懸浮控件的顯隱等,不然人家都退出這個(gè)界面了窗慎,咱們的購物車還擱下面顯示著物喷。個(gè)人認(rèn)為這玩意還是更適合Popupindow
和全局自定義Dialog
之類的。
那么Flutter
中有沒有方便管理一堆子組件的組件呢?
在編寫Flutter
應(yīng)用的時(shí)候脯丝,我們程序的入口是通過main()
函數(shù)的runApp(MyApp())
執(zhí)行的商膊,MyApp
通常會build
出一個(gè)MaterialApp
組件
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '我要點(diǎn)東西',
home: HomePage(),
);
}
}
對于不同界面之間的路由我們會交由Navigator
管理,比如: Navigator.push
和 Navigator.pop
等宠进。為什么MaterialApp
能夠?qū)?code>Navigator的操作作出感應(yīng)呢晕拆?
MaterialApp
的構(gòu)造方法中有這么一個(gè)字段navigatorKey
class MaterialApp extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey;
///省略一些代碼
}
class _MaterialAppState extends State<MaterialApp> {
@override
Widget build(BuildContext context) {
Widget result = WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: _navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return MaterialPageRoute<T>(settings: settings, builder: builder);
},
///省略一些代碼
}
}
往深入的去看它會傳遞給WidgetsApp
構(gòu)造方法中的navigatorKey
,WidgetsApp
的navigatorKey
在組件初始化時(shí)會默認(rèn)的創(chuàng)建一個(gè)全局的NavigatorState
材蹬,然后對build(BuildContext context)
中創(chuàng)建的Navigator
進(jìn)行狀態(tài)管理实幕。
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
_updateNavigator();
_locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this);
}
// NAVIGATOR
GlobalKey<NavigatorState> _navigator;
void _updateNavigator() {
//MaterialApp中不指定navigatorKey會默認(rèn)初始化一個(gè)全局的NavigatorState
_navigator = widget.navigatorKey ?? GlobalObjectKey<NavigatorState>(this);
}
@override
Widget build(BuildContext context) {
//這里會構(gòu)建出一個(gè)Navigator組件,并把上面的navigatorKey寫進(jìn)去堤器,這樣就做到了Navigator的棧操作
Widget navigator;
if (_navigator != null) {
navigator = Navigator(
key: _navigator,
// If window.defaultRouteName isn't '/', we should assume it was set
// intentionally via `setInitialRoute`, and should override whatever
// is in [widget.initialRoute].
initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
? WidgetsBinding.instance.window.defaultRouteName
: widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
onGenerateRoute: _onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
? Navigator.defaultGenerateInitialRoutes
: (NavigatorState navigator, String initialRouteName) {
return widget.onGenerateInitialRoutes(initialRouteName);
},
onUnknownRoute: _onUnknownRoute,
observers: widget.navigatorObservers,
);
}
}
}
到這里基本上可以想到該如何實(shí)現(xiàn)底部購物車的功能了昆庇。
是的,我們可以在點(diǎn)單界面自定義一個(gè)Navigator
來管理搜索商品闸溃、商品詳情整吆、商品購物車列表等路由的跳轉(zhuǎn),其它的交由我們MaterialApp
的Navigator
控制辉川。
下面是功能代碼大致實(shí)現(xiàn):
class OrderPage extends StatefulWidget {
@override
_OrderPageState createState() => _OrderPageState();
}
class _OrderPageState extends State<OrderPage> {
///管理點(diǎn)單功能Navigator的key
GlobalKey<NavigatorState> navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
//監(jiān)聽系統(tǒng)返回鍵表蝙,先對自定義Navigator里的路由做出棧處理,最后關(guān)閉OrderPage
navigatorKey.currentState.maybePop().then((value) {
if (!value) {
NavigatorUtils.goBack(context);
}
});
return Future.value(false);
},
child: Stack(
children: <Widget>[
Navigator(
key: navigatorKey,
onGenerateRoute: (settings) {
if (settings.name == '/') {
return PageRouteBuilder(
opaque: false,
pageBuilder:
(childContext, animation, secondaryAnimation) =>
//構(gòu)建內(nèi)容層
_buildContent(childContext),
transitionsBuilder:
(context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
transitionDuration: Duration(milliseconds: 300),
);
}
return null;
},
),
Positioned(
bottom: 0,
right: 0,
left: 0,
//購物車組件乓旗,位于底部
child: ShopCart(),
),
//添加商品進(jìn)購物車的小球動(dòng)畫
ThrowBallAnim(),
],
),
);
}
}
頁面過渡動(dòng)畫Hero的使用
效果可以看最開始的那一張GIF府蛇。
Hero
的使用非常的簡單,需要關(guān)聯(lián)的兩個(gè)組件用Hero
組件包裹屿愚,并指定相同的tag
參數(shù),代碼如下:
///列表item
InkWell(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Hero(
tag: widget.data,
child: LoadImage(
'${widget.data.img}',
width: 81.0,
height: 81.0,
fit: BoxFit.fitHeight,
),
),
),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => GoodsDetailsPage(data: widget.data)));
},
);
///詳情
Hero(
tag: tag,
child: LoadImage(
imageUrl,
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
)
是不是覺得這樣寫好就完事了呢汇跨,Hero的效果就會出來了?在正常情況下是會有效果妆距,但是在我們這里卻沒有任何效果穷遂,就跟普通的路由跳轉(zhuǎn)一樣樣的,這是為啥呢娱据?
我們在MaterialApp
中的是有效果的塞颁,自定義的Navigator
的卻沒效果,那么肯定是MaterialApp
的Navigator
做了什么配置吸耿。
還是通過MaterialApp
的源碼可以發(fā)現(xiàn)祠锣,在其初始化的時(shí)候會new一個(gè)HeroController
并在構(gòu)造參數(shù)navigatorObservers
中添加進(jìn)去
class _MaterialAppState extends State<MaterialApp> {
HeroController _heroController;
@override
void initState() {
super.initState();
_heroController = HeroController(createRectTween: _createRectTween);
_updateNavigator();
}
@override
void didUpdateWidget(MaterialApp oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.navigatorKey != oldWidget.navigatorKey) {
// If the Navigator changes, we have to create a new observer, because the
// old Navigator won't be disposed (and thus won't unregister with its
// observers) until after the new one has been created (because the
// Navigator has a GlobalKey).
_heroController = HeroController(createRectTween: _createRectTween);
}
_updateNavigator();
}
List<NavigatorObserver> _navigatorObservers;
void _updateNavigator() {
if (widget.home != null ||
widget.routes.isNotEmpty ||
widget.onGenerateRoute != null ||
widget.onUnknownRoute != null) {
_navigatorObservers = List<NavigatorObserver>.from(widget.navigatorObservers)
..add(_heroController);
} else {
_navigatorObservers = const <NavigatorObserver>[];
}
}
///....
}
最終是添加進(jìn)WidgetsApp
構(gòu)建的Navigator
構(gòu)造參數(shù)observers
里
navigator = Navigator(
key: _navigator,
// If window.defaultRouteName isn't '/', we should assume it was set
// intentionally via `setInitialRoute`, and should override whatever
// is in [widget.initialRoute].
initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
? WidgetsBinding.instance.window.defaultRouteName
: widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
onGenerateRoute: _onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
? Navigator.defaultGenerateInitialRoutes
: (NavigatorState navigator, String initialRouteName) {
return widget.onGenerateInitialRoutes(initialRouteName);
},
onUnknownRoute: _onUnknownRoute,
//MaterialApp的HeroController會添加進(jìn)去
observers: widget.navigatorObservers,
);
所以我們只要同理在自己定義的Navigator
里添加進(jìn)去即可:
Stack(
children: <Widget>[
Navigator(
key: navigatorKey,
//自定Navigator使用不了Hero的解決方案
observers: [HeroController()],
onGenerateRoute: (settings) {
if (settings.name == '/') {
return PageRouteBuilder(
opaque: false,
pageBuilder:
(childContext, animation, secondaryAnimation) =>
_buildContent(childContext),
transitionsBuilder:
(context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
transitionDuration: Duration(milliseconds: 300),
);
}
return null;
},
),
Positioned(
bottom: 0,
right: 0,
left: 0,
child: ShopCart(),
),
//添加商品進(jìn)購物車的小球動(dòng)畫
ThrowBallAnim(),
],
)
高斯模糊的實(shí)現(xiàn)
底部購物車的灰色區(qū)域使用到了高斯模糊的效果
該效果在Flutter
中的控件是BackdropFilter
,用法如下:
BackdropFilter(
filter: ImageFilter.blur(sigmaX, sigmaY),
child: ...)
不過使用的時(shí)候也有小坑,如果沒有進(jìn)行剪輯,那么高斯模糊的效果會擴(kuò)散至全屏咽安,正確的寫法應(yīng)該如下:
ClipRect(
BackdropFilter(
filter: ImageFilter.blur(sigmaX, sigmaY),
child: ...)
)
ps:其實(shí)在BackdropFilter
的源碼中有更詳細(xì)的說明伴网,建議大家去看看
商品欄目分類的實(shí)現(xiàn)
商品欄目的分類說的籠統(tǒng)點(diǎn)就是一、二級菜單對PageView
的page切換處理妆棒。
可以把上圖右側(cè)框出的部分看成一個(gè)PageView
澡腾,左側(cè)tab
的點(diǎn)擊就是對PageView
進(jìn)行的一個(gè)豎直方向的page切換操作沸伏,對應(yīng)的tab
下沒有二級tab
的話,那么當(dāng)前page展示的就是一個(gè)ListView
动分。
那如果有二級tab
的話毅糟,當(dāng)前page展示的是TabBar
+PageView
聯(lián)動(dòng),這個(gè)PageView
的方向是橫向水平的
如果上述的描述還不是很懂的話澜公,沒關(guān)系姆另,我準(zhǔn)備了一張總的結(jié)構(gòu)圖,清晰的描述了它們之間的關(guān)系:
還有一點(diǎn)需要注意的地方坟乾,我們不希望每次切換tab
的時(shí)候,Widgets
都會重新加載一次迹辐,這樣對用戶的體驗(yàn)是極差的,我們要對已經(jīng)加載過的page保持它的一個(gè)頁面狀態(tài)甚侣。這一點(diǎn)使用AutomaticKeepAliveClientMixin
可以做到明吩。
class SortRightPage extends StatefulWidget {
final int parentId;
final List<Sort> data;
SortRightPage(
{Key key,
this.parentId,
this.data})
: super(key: key);
@override
_SortRightPageState createState() => _SortRightPageState();
}
class _SortRightPageState extends State<SortRightPage>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
if (widget.data == null || widget.data.isEmpty) {
if (widget.parentId == -1) {
//套餐Page
return DiscountPage();
} else {
//商品列表
return SubItemPage(
key: Key('subItem${widget.parentId}'),
id: widget.parentId
);
}
} else {
//二級分類
return SubListPage(
key: Key('subList${widget.parentId}'),
data: widget.data
);
}
}
@override
bool get wantKeepAlive => true;
}
結(jié)束
好了,文章到這里七七八八的差不多了,其他更加細(xì)節(jié)的地方大家可以去Github上看我寫的demo殷费,里面對用戶交互的處理還是蠻妥當(dāng)?shù)挠±螅M軌驇椭酱蠹摇?/p>