倒敘手法,先上效果圖:

最初的想法是使用系統(tǒng)自帶的BottomNavigationBar來實現(xiàn),做到一半發(fā)現(xiàn)完全無法滿足這奇葩合理的需求:
- 背景模糊效果
- 凸起按鈕
- 非標準高度
-
indicator動畫
起初也曾想到過想要封裝一個相對通用的組件,然而需求太過非標,不可避免地需要重復造車輪子,只能將其中一些思路整理出來。
框架結(jié)構(gòu)
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
alignment: AlignmentDirectional.bottomStart,
children: [
// 頁面
Container(
child: PageView(),
),
// 底部導航
LATabBar(),
// indicator動畫
Transform.translate(),
],
),
);
}
我們使用Stack 來放置子視圖,簡單粗暴,從底部開始層疊:
- Container:子頁面,使用
PageView進行頁面切換 - LATabBar:自定義底部導航
- Transform.translate():
indicator動畫組件
出于盡可能解耦的原因,這里把indicator 放到底部導外。
接下來,我們將重點關(guān)注底部導航的構(gòu)建。
全局常量
定義全局常量 ,以針對不同設備進行適配。
底部導航
考慮到這里有一個模糊效果,使用Stack來層疊background和item ,首先來定義相關(guān)屬性。
class LATabBar extends StatefulWidget {
final double height;
final Color backgroundColor;
final Widget background;
final List<LATabItem> items;
...
}
- height:底部導航視圖高度
- backgroundColor:背景顏色(考慮后續(xù)組件化設計)
- background:自定義背景組件(模糊+凸起效果)
- items:自定義NavigationItem
自定義背景
先前說過,此方法很難封裝成一個通用的組件。因為根據(jù)需求,需要自定義的東西太多,標準組件所需默認值也多。因此,只能將其中的思路記錄下來。
繪制不規(guī)則圖形
在iOS開發(fā)中,此類不規(guī)則背景,只需使用一個UIImageView 加載帶透明度圖片,隨后使用view.mask 即可實現(xiàn)不規(guī)則背景。
而在Flutter 中,并無相應API來渲染(我太蔡了),因此,需要使用到自定義裁切ClipPath 來繪制相應不規(guī)則圖形。
首先使用貝塞爾曲線,自定義一個clipper:
class RaisedClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
/**凸起高度*/
double raisedHeight = 18;
/**凸起寬度*/
double raisedWidth = 80;
/**凸起右邊距*/
double paddingTrailing = 25;
Path path = Path();
path.lineTo(0, raisedHeight);
double cubicStartX = size.width - paddingTrailing - raisedWidth - 10;
// 貝塞爾曲線起始位置
path.lineTo(cubicStartX, raisedHeight);
// 繪制曲線
path.cubicTo(
cubicStartX + raisedWidth * 0.1, raisedHeight,
cubicStartX + raisedWidth * 0.25, 0,
cubicStartX + raisedWidth * 0.5, 0);
path.cubicTo(
cubicStartX + raisedWidth * 0.75, 0,
cubicStartX + raisedWidth * 0.85, raisedHeight,
size.width - paddingTrailing, raisedHeight);
path.lineTo(size.width, raisedHeight);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
return path;
}
關(guān)于貝塞爾曲線,這里不再贅述,分享一個在線預覽工具,方便調(diào)試。
模糊效果
Flutter提供了BackdropFilter 來作為高斯模糊的組件,配合使用ImageFilter 來制作模糊效果。
class BlurBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ClipPath(
clipper: RaisedClipper(),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: Container(
color: Colors.white.withOpacity(0.5),
),
),
);
}
}
這樣就完成了自定義底部導航背景視圖的編碼。
導航切換按鈕
先上代碼:
enum LATabItemStyle {
normal,
titleOnly,
iconOnly,
}
class LATabItem extends StatefulWidget {
final double width;
final String iconNormal;
final String iconSelected;
final EdgeInsets iconInsets;
final Alignment iconAlignment;
final Color titleNormalColor;
final Color titleSelectedColor;
final EdgeInsets titleInsets;
final Alignment titleAlignment;
final String title;
final GestureTapCallback onTap;
final bool selected;
final LATabItemStyle style;
const LATabItem({
Key key,
this.width,
this.iconNormal,
this.iconSelected,
this.iconInsets = EdgeInsets.zero,
this.iconAlignment = Alignment.center,
this.titleNormalColor,
this.titleSelectedColor,
this.titleInsets = EdgeInsets.zero,
this.titleAlignment = Alignment.center,
this.title,
this.onTap,
this.selected = false,
this.style = LATabItemStyle.normal}) : super(key: key);
@override
State<StatefulWidget> createState() {
return LATabItemState();
}
}
class LATabItemState extends State<LATabItem> {
@override
Widget build(BuildContext context) {
Widget content;
switch (this.widget.style) {
case LATabItemStyle.titleOnly:
break;
case LATabItemStyle.iconOnly:
content = Container(
width: this.widget.width,
alignment: this.widget.iconAlignment,
padding: this.widget.iconInsets,
child: Image.asset(this.widget.selected ? this.widget.iconSelected : this.widget.iconNormal),
);
break;
default:
break;
}
if (this.widget.width == null || this.widget.width <= 0) {
return Expanded(
child: GestureDetector(
onTap: this.widget.onTap,
child: content,
behavior: HitTestBehavior.opaque,
),
);
} else {
return GestureDetector(
onTap: this.widget.onTap,
child: content,
);
}
}
}
導航按鈕完全可以使用自定義的widget 而不必拘泥于形式,可以是自定義的文字、圖片、繪制的圖形各種。只要將其作為GestureDetector 的child 即可響應點擊事件。
需要注意的是,如果使用自適應大小的Expanded ,則需要指定GestureDetector 的behavior: HitTestBehavior.opaque 。
indicator動畫
簡單動畫,直接使用AnimationController 及Tween 來制作動畫。
- 初始化:
_animationController = AnimationController(duration: Duration(milliseconds: 100), vsync: this);
_tween = Tween(begin: leading, end: leading);
_animation = _tween.animate(_animationController)
..addListener(() {
setState(() {});
});
-
Translation容器:
Transform.translate(
offset: Offset(_animation.value, 0),
child: Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, Global.tabBarHeight + Global.paddingBottom - Global.tabBarRaisedHeight - 4),
width: 20,
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(2)),
color: Color(0xFFB60005),
),
),
),
- 切換視圖時,執(zhí)行動畫:
void setIndicator(int index) {
_tween.begin = _tween.end;
_animationController.reset();
_tween.end = unitWidth * index + leading;
_animationController.forward();
}
到這里,自定義底部導航完成。
最后放上demo。