之前,也寫過幾篇關於 Flutter
的博文,最近,又花了一些時間學習研究 Flutter
,完成了高仿大廠 App 專案 (專案使用的介面都是來自線上真實App抓包而來,可以做到和線上專案相同的效果),也總結積累了一些小技巧和知識點,所以,在這裡記錄分享出來,也希望 Flutter
生態越來越好 (flutter開發App效率真的很高,開發體驗也是很好的 ?)。
以下博文會分為4個部分概述:
- 專案完成的功能預覽
- 專案結構分析
- 專案功能詳細概述(所用知識點)
- 小技巧積累總結
專案完成的功能預覽
首先,我們來通過一個視訊來快速預覽下專案完成的功能和執行效果,如下
大家看完視訊,大概瞭解到,完成度基本可以和線上的App相差無異了,大家如果對專案感興趣,想了解具體怎麼實現的,可以去我的 GitHub clone 原始碼檢視。
本視訊是用真機錄屏的,因為,語音搜尋功能需要錄音,模擬器無法錄音,當然, iOS
和 Andorid
都可以執行,效果是一樣的,如圖:
專案結構分析
其次,梳理下專案的目錄結構,理解每個檔案都是幹什麼的,我們先來看看一級目錄,如下:
├── 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原始碼
語音搜尋頁面
語音搜尋頁面效果如圖:由於模擬器無法錄音,所以無法展示正常流程,如果錄音識別成功後會返回搜尋頁面,在專案預覽視訊中可以看到正常流程。
語音搜尋功能使用的是百度的語言識別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元件
效果如圖:
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導航效果如圖:
icon導航浮動在banner之上,其實用的是 flutter
的 Stack 元件,Stack 元件能讓其子元件堆疊顯示,它通常和 Positioned 元件配合使用,佈局結構程式碼如下:
ListView(
children: <Widget>[
Container(
child: Stack(
children: <Widget>[
Container( ... ), //這裡放的是banner的程式碼
Positioned( ... ), //這個就是icon導航,通過 Positioned 固定顯示位置
],
),
),
Container( ... ), // 這裡放的網格導航及其他
],
),
複製程式碼
漸變不規則帶有背景圖的網格導航
網格導航效果如圖:
如圖,網格導航分為三行四欄,而第一行分為三欄,每一行的第一欄寬度大於其餘三欄,其餘三欄均等,每一行都有漸變色,而且第一、二欄都有背景圖;
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檢視
這裡需要注意的是:展開顯示更多span標籤元件的實現,因為,這個元件在很多的其他元件裡用到而且要根據介面資料動態渲染,且元件自身存在狀態的變化,這種情況下,最好是把他單獨封裝成一個元件(widget),否則,很難控制自身狀態的變化,出現點選沒有效果,或點選影響其他元件。
目的地搜尋頁面
效果如圖:點選搜尋結果,如:點選‘一日遊‘,會搜尋到‘一日遊‘的相關資料
目的地搜尋頁面,大多都是和佈局和對接介面的程式碼,在這裡就不再贅述。
然後就是旅拍頁面功能及所用知識點,重點看下以下功能實現:
- 左右佈局tabBarListView
- 瀑布流卡片
- 旅拍搜尋頁
左右佈局tabBarListView
效果如圖:可左右滑動切換頁面,上拉載入更多,下拉重新整理等
這個是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;
}
複製程式碼
暫時只能想到這些常用的知識點,以後如有新的會慢慢補充。
部落格地址: lishaoy.net
部落格Notes地址: h.lishaoy.net
專案GitHub地址: github.com/persilee/fl…