以前,也寫過幾篇關於 Flutter
的博文,最近,又花了一些時間學習研究 Flutter
,完成了高仿大廠 App 項目 (項目使用的接口都是來自線上真實App抓包而來,能夠作到和線上項目相同的效果),也總結積累了一些小技巧和知識點,因此,在這裏記錄分享出來,也但願 Flutter
生態愈來愈好 (flutter開發App效率真的很高,開發體驗也是很好的 🙂)。android
如下博文會分爲3個部分概述:ios
其次,梳理下項目的目錄結構,理解每一個文件都是幹什麼的,咱們先來看看一級目錄,以下:git
├── README.md # 描述文件 ├── android # android 宿主環境 ├── build # 項目構建目錄,由flutter自動完成 ├── flutter_ctrip.iml ├── fonts # 本身建立的目錄,用於存放字體 ├── images # 本身建立的目錄,用於存放圖片 ├── ios # iOS 宿主環境 ├── lib # flutter 執行文件,本身寫的代碼都在這 ├── pubspec.lock # 用來記錄鎖定插件版本 ├── pubspec.yaml # 插件及資源配置文件 └── test # 測試目錄
這個就不用多解釋,大可能是 flutter 生成及管理的,咱們須要關注的是 lib 目錄。github
咱們再來看看二級目錄,以下 (重點關注下lib目錄)web
├── 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 目錄下二級目錄,看看整個項目建立了多少個文件,寫了多少代碼,以下 (其實,並非不少)json
├── 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 源碼運行起來,天然就清除了)。bash
首先,來看看首頁功能及所用知識點,首頁重點看下如下功能實現:服務器
先來看看具體效果,一睹芳容,如圖:app
滾動的時候 appBar 背景色從透明變成白色或白色變成透明,這裏主要用了 flutter 的 NotificationListener
組件,它會去監聽組件樹冒泡事件,當被它包裹的的組件(子組件) 發生變化時,Notification
回調函數會被觸發,因此,經過它能夠去監聽頁面的滾動,來動態改變 appBar 的透明度(alpha),代碼以下:ide
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查看
Tips:
這裏須要注意的是:展開顯示更多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; }
暫時只能想到這些經常使用的知識點,之後若有新的會慢慢補充。
博客地址: https://lishaoy.net
博客Notes地址: https://h.lishaoy.net
項目GitHub地址: https://github.com/persilee/flutter_ctrip