底部導航是常見的APP佈局方式,實際上我本身經常使用的app都是底部導航的。Android和iOS都有官方組件可供使用。Flutter也有BottomNavigationBar,使用時有踩坑,這裏記錄一下。html
普通實現:java
BottomNavigationBar botttomNavBar = BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.code), title: Text('code')),
BottomNavigationBarItem(icon: Icon(Icons.add), title: Text('add')),
BottomNavigationBarItem(icon: Icon(Icons.print), title: Text('print'))
],
currentIndex: _currentIndex,
type: BottomNavigationBarType.fixed,
onTap: (int index) {
setState(() {
_currentIndex = index;
});
},
);
複製代碼
問:看起來很簡單,至於分析這麼多嗎?android
答:emmmm,這實現優勢是設計標準規範,官方組件也簡單穩定可靠。但前提是設計師接受這種設定(即便是fixed,選中圖標和文字也會有放大縮小動畫),至少中國主流的APP,navigation item都是fixed並且沒有動畫,官方組件並不提供這種選擇。ios
既然設計師有要求那不能慫,分析是由於內部的_BottomNavigationTile做祟,那本身實現navigationItem控制是否選中,而且不傳currentIndex給BottomNavigationBar,應該能夠吧git
Widget _buildBottomNavigationBar() {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
_buildItem(icon: Icons.code, tabItem: TabItem.code),
_buildItem(icon: Icons.add, tabItem: TabItem.add),
_buildItem(icon: Icons.print, tabItem: TabItem.print),
],
onTap: _onSelectTab,
);
}
// 用定製化的icon和tabItem構建BottomNavigationBarItem
BottomNavigationBarItem _buildItem({IconData icon, TabItem tabItem}) {
String text = tabItemName(tabItem);
return BottomNavigationBarItem(
icon: Icon(
icon,
color: _colorTabMatching(item: tabItem),
),
title: Text(
text,
style: TextStyle(
color: _colorTabMatching(item: tabItem),
),
),
);
}
// 切換item的顏色,選中用primaryColor,其餘都是grey
Color _colorTabMatching({TabItem item}) {
return currentItem == item ? Theme.of(context).primaryColor : Colors.grey;
}
複製代碼
問:效果如何?github
答:嗯,不錯。等等。。。啊,怎麼有個大一點。沒道理啊,事出蹊蹺必有妖,須要從源碼中找答案了。下圖的home明顯比mail大,對吧? 編程
主要代碼都在bottom_navigation_bar.dart裏,bottom_navigation_bar_item.dart是item的定義bash
至關因而一個自定義的Button,用來放在BottomNavigationBar上,它實現了Material(Android)和Cupertino(iOS)兩種風格。app
Scaffold是Root Widget- MaterialApp的腳手架。封裝了Material Design App會用到的AppBar,Drawer,SnackBar,BottomNavigationBar等。BottomNavigationBarType有fixed 和shifting兩種樣式,超過3個纔會有區別,通常爲了體驗一致,咱們會用fixed type。iphone
BottomNavigationBar是一個StatefulWidget,能夠按如下步驟分析這種組件:1,先看它持有的狀態,2,看下他的生命週期實現,3,再仔細分析它的build方法。
List<AnimationController> _controllers = <AnimationController>[];
List<CurvedAnimation> _animations;
// A queue of color splashes currently being animated.
final Queue<_Circle> _circles = Queue<_Circle>();
// Last splash circle's color, and the final color of the control after
// animation is complete.
Color _backgroundColor;
複製代碼
前面三個屬性都和動畫相關,第四個是設背景。
問:BottomNavigationBar爲何沒有變量標記當前哪一個item選中?
答:函數式編程一個原則是要函數儘可能純,currentIndex這個屬性依賴外邊傳入,每次變化從新觸發Render。若是本身維護,則還須要提供一個回調方法供外部調用,返回最新的currentIndex值。
// 初始化操做,具體實現再resetState裏,對上面的這些狀態屬性初始化操做
@override
void initState() {
super.initState();
_resetState();
}
// 回收資源操做,通常用到動畫都須要的
@override
void dispose() {
for (AnimationController controller in _controllers)
controller.dispose();
for (_Circle circle in _circles)
circle.dispose();
super.dispose();
}
// 當屬性變化時Flutter系統回調該方法。當item數量變化時直接從新初始化;當index變化,作相應動畫。
@override
void didUpdateWidget(BottomNavigationBar oldWidget) {
super.didUpdateWidget(oldWidget);
// No animated segue if the length of the items list changes.
if (widget.items.length != oldWidget.items.length) {
_resetState();
return;
}
if (widget.currentIndex != oldWidget.currentIndex) {
switch (widget.type) {
case BottomNavigationBarType.fixed:
break;
case BottomNavigationBarType.shifting:
_pushCircle(widget.currentIndex);
break;
}
_controllers[oldWidget.currentIndex].reverse();
_controllers[widget.currentIndex].forward();
}
if (_backgroundColor != widget.items[widget.currentIndex].backgroundColor)
_backgroundColor = widget.items[widget.currentIndex].backgroundColor;
}
// 下面分析
@override
Widget build(BuildContext context) {}
複製代碼
注意:initState裏有個操做比較隱蔽:_controllers[widget.currentIndex].value = 1.0;
@override
Widget build(BuildContext context) {
// debug 檢查
assert(debugCheckHasDirectionality(context));
assert(debugCheckHasMaterialLocalizations(context));
// Labels apply up to _bottomMargin padding. Remainder is media padding.
final double additionalBottomPadding = math.max(MediaQuery.of(context).padding.bottom - _kBottomMargin, 0.0);
// 根據BottomNavigationBarType設背景色,shifting纔會有
Color backgroundColor;
switch (widget.type) {
case BottomNavigationBarType.fixed:
break;
case BottomNavigationBarType.shifting:
backgroundColor = _backgroundColor;
break;
}
return Semantics( // Semantics用來實現無障礙的
container: true,
explicitChildNodes: true,
child: Stack(
children: <Widget>[
Positioned.fill(
child: Material( // Casts shadow.
elevation: 8.0,
color: backgroundColor,
),
),
ConstrainedBox(
constraints: BoxConstraints(minHeight: kBottomNavigationBarHeight + additionalBottomPadding),
child: Stack(
children: <Widget>[
Positioned.fill( // 點擊時的圓形類波紋動畫
child: CustomPaint(
painter: _RadialPainter(
circles: _circles.toList(),
textDirection: Directionality.of(context),
),
),
),
Material( // Splashes.
type: MaterialType.transparency,
child: Padding(
padding: EdgeInsets.only(bottom: additionalBottomPadding),
child: MediaQuery.removePadding(
context: context,
removeBottom: true,
// tiles就是_BottomNavigationTile,裏面放BottomNavigationBarItem
child: _createContainer(_createTiles()),
)))]))]));
}}
複製代碼
Widget _buildIcon() {
...
// 構建Iocn
}
Widget _buildFixedLabel() {
....
// 騷操做,用矩陣來給文字做動畫,更平滑
// The font size should grow here when active, but because of the way
// font rendering works, it doesn't grow smoothly if we just animate
// the font size, so we use a transform instead.
child: Transform(
transform: Matrix4.diagonal3(
Vector3.all(
Tween<double>(
begin: _kInactiveFontSize / _kActiveFontSize,
end: 1.0,
).evaluate(animation),
),
),
alignment: Alignment.bottomCenter,
child: item.title,
),
),
),
);
}
Widget _buildShiftingLabel() {
return Align(
.....
// shifting的label是fade動畫,只有當前選中的纔會顯示label
child: FadeTransition(
alwaysIncludeSemantics: true,
opacity: animation,
child: DefaultTextStyle.merge(
style: const TextStyle(
fontSize: _kActiveFontSize,
color: Colors.white,
),
child: item.title,
),
),
),
);
}
@override
Widget build(BuildContext context) {
int size;
Widget label;
// 生成不一樣的label
switch (type) {
case BottomNavigationBarType.fixed:
size = 1;
label = _buildFixedLabel();
break;
case BottomNavigationBarType.shifting:
size = (flex * 1000.0).round();
label = _buildShiftingLabel();
break;
}
return Expanded(
....
children: <Widget>[
_buildIcon(),
label,
],
),
),
Semantics(
label: indexLabel,
}
複製代碼
經過分析分析源碼,發現緣由是bottomNavigationBarState的initState裏_controllers[widget.currentIndex].value = 1.0
設了currentIndex item動畫的初值,currentIndex的默認值是0,因此第一個圖標會大一點點。這個問題也有比較雞賊的手法能夠處理(魔改源碼什麼~),但這樣你們都以爲不妥。同事眉頭一皺,作了一個大膽的決定,不用系統組件BottomNavigationBar,本身封裝一下:
// SafeArea來兼容下iPhone X,android和iOS陰影不同,因此區分下。
Widget _buildBottomNavigationBar() {
return SafeArea(
child: SizedBox(
height: 50.0,
child: Card(
color: Platform.isIOS ? Colors.transparent : Colors.white,
elevation: Platform.isIOS ? 0.0 : 8.0,
// iphone 無陰影
shape: RoundedRectangleBorder(),
margin: EdgeInsets.all(0.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Divider(),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_buildBottomItem(
image: HImages.home, text: '首頁', index: 0),
_buildBottomItem(
image: HImages.stats, text: '數據', index: 1),
_buildBottomItem(
image: HImages.mine, text: '個人', index: 3)
]),
)
]))));
}
// 封裝的BottomItem,選中顏色爲primaryColor,未選中grey。點擊波紋效果InkResponse
Widget _buildBottomItem({String image, String text, int index}) {
Color color =
currentIndex == index ? Theme.of(context).primaryColor : Colors.grey;
return Expanded(
child: InkResponse(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Image.asset(image, color: color, width: 22.0, height: 22.0),
Text(text, style: TextStyle(color: color, fontSize: 10.0))
]),
onTap: () => setState(() => currentIndex = index)));
}
複製代碼
問:這該是最終版了吧?*
答:Naive,是連iPhone X都考慮了,但細節漸變顏色,platform特性支持尚未。。。說到特性我就佛了,一佛,我就想起西天取經,明年年初,中美合拍的西遊記即將正式開機,我繼續扮演美猴王孫悟空,我會用美猴王藝術形象努力創造一個正能量的形象,文體兩開花,弘揚中華文化,但願你們多多關注。
本文源碼地址:https://github.com/hyjfine/flutter-play
(完)
@子路宇, 本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。