开始
想做个有意思的底部导航栏,不想用官方的,找了一些参考,发现这个比较有意思,还有动画。
遗憾的但是拿过来这个并不能直接用
思路
在布局上来看 底部导航栏是一个横向的布局 我们用 Row 就可以了,每Bar切换是与页面绑定的,而且Bar的个数 是不固定的,并且每个Bar都要与对应的页面相绑定。
新建一个 Bar的类存储每个导航栏的信息
class TabItem {
final String title;
final IconData iconData;
TabItem({
required this.iconData,
required this.title,
});
}
当然你还可以扩充 比如设置这个加个颜色什么之类的 。
简单的底部导航栏
效果
通过传入的导航,根据导航数量进行构建底部导航栏,barItems里添加每个导航的样式。
List<Widget> _buildBarItems() {
List<Widget> barItems = [];
for (int i = 0; i < widget.tabItem.length; i++) {
barItems.add(Expanded(
child: Stack(
fit: StackFit.expand,
children: [
SizedBox(
height: double.infinity,
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
widget.tabItem[i].title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
)
),
SizedBox(
height: double.infinity,
width: double.infinity,
child:IconButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: const EdgeInsets.all(0),
alignment: const Alignment(-0.8, 1.5),
icon: Icon(
widget.tabItem[i].iconData,
color: widget.iconBackgroundColor,
),
onPressed: () {
setState(() {
barIndex = i;
widget.onBarTap(barIndex);
});
},
),
)
],
),
));
}
return barItems;
}
把导航栏这个放入Row 横向布局里即可
class EasyBar extends StatefulWidget {
final Color? iconBackgroundColor;
final Color? backgroundColor;
final List<TabItem> tabItem;
final Function onBarTap;
const EasyBar(
{Key? key,
this.iconBackgroundColor = Colors.deepPurple,
required this.tabItem,
required this.onBarTap,
this.backgroundColor = Colors.white})
: super(key: key);
@override
State<EasyBar> createState() => _EasyBarState();
}
class _EasyBarState extends State<EasyBar> with TickerProviderStateMixin {
double fabIconAlpha = 1;
///是1的话在中间
int barIndex = 1;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
alignment: Alignment.topCenter,
children: <Widget>[
Container(
height: 65,
padding: const EdgeInsets.only(bottom: 18),
decoration: BoxDecoration(
color: widget.backgroundColor,
boxShadow: const [BoxShadow(color: Colors.black12, offset: Offset(0, -1), blurRadius: 8)]),
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildBarItems(),
),
),
],
);
}
把点击事件暴露出来处理,最主要的是页面的index 要与导航的index所绑定
int selectedBarIndex = 1;
List<Widget> pages = [];
@override
void initState() {
super.initState();
pages
..add(Scaffold(
body: Container(width: double.infinity, height: double.infinity, color: Colors.lightBlue,),
))
..add(Scaffold(body: Container(width: double.infinity, height: double.infinity, color: Colors.pinkAccent)))
..add(Scaffold(
body: Container(width: double.infinity, height: double.infinity, color: Colors.amber,),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: pages[selectedBarIndex],
bottomNavigationBar: EasyBar(
backgroundColor: Colors.pink,
tabItem: widget.barItems,
onBarTap: (index) {
setState(() {
//底部的index和页面绑定
selectedBarIndex = index;
});
},
),
);
}
动画的底部导航栏
效果
通过传入的导航的数量构建底部导航栏还是大同小异,唯一的区别是,当选中的时候 icon变透明,以及title从底部移动上来的两个动画效果 使用了 AnimatedOpacity 控制透明度 AnimatedAlign 来控制标题的位置
List<Widget> _buildBarItems() {
List<Widget> barItems = [];
for (int i = 0; i < widget.tabItem.length; i++) {
barItems.add(Expanded(
child: Stack(
fit: StackFit.expand,
children: [
SizedBox(
height: double.infinity,
width: double.infinity,
child: AnimatedAlign(
// curve: Curves.easeIn,//动画曲线
duration: Duration(milliseconds: widget.animationMilliseconds!),
alignment: Alignment(0, (barIndex == i)? 3 : 8),//控制文字的位置
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
widget.tabItem[i].title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
)),
),
SizedBox(
height: double.infinity,
width: double.infinity,
child:AnimatedOpacity(
duration: Duration(milliseconds: widget.animationMilliseconds!),//动画时长
opacity: (barIndex == i) ? 0 : 1,////控制icon的透明度
child: IconButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: const EdgeInsets.all(0),
alignment: const Alignment(0, 0),
icon: Icon(
widget.tabItem[i].iconData,
color: widget.iconBackgroundColor,
),
onPressed: () {
setState(() {
barIndex = i;
widget.onBarTap(barIndex);
_positionTween.begin = _positionAnimation.value; //叠叠圆X轴开始位置
_positionTween.end = i - 1; //叠叠圆X轴结束的位置
_animationController.reset();
_fadeOutController.reset();
_animationController.forward();
_fadeOutController.forward();
});
},
),
),
)
],
),
));
}
return barItems;
}
这个圆我们观察他! 大圆叠小圆 上面还有个icon
叠叠圆
Positioned(
top: -45,
left: 0,
right: 0,
child: IgnorePointer(
child: Container(
decoration: const BoxDecoration(color: Colors.transparent),
child: Align(
heightFactor: 0.5,
alignment: Alignment(_positionAnimation.value, -1),
//_positionAnimation.value控制这个叠叠的圆的位置,根据点的导航修改圆的x轴的位置
child: FractionallySizedBox(
widthFactor: 1/3,//控制占的宽度
child: Stack(
alignment: Alignment.center,
children: <Widget>[
SizedBox(
height: 90,
width: 90,
child: ClipRect(
clipper: HalfClipper(),//裁剪
child: Center(
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: widget.backgroundColor,
shape: BoxShape.circle,
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 8)])),
)),
),
SizedBox(
height: 60,
width: 60,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.iconBackgroundColor,
border: Border.all(color: Colors.white, width: 5, style: BorderStyle.none)),
child: Padding(
padding: const EdgeInsets.all(0.0),
child: Opacity(
opacity: fabIconAlpha,//白色图标切换时的动画
child: Icon(
widget.tabItem[barIndex].iconData,
color: Colors.white,
),
),
),
),
)
],
),
),
),
),
),
),
要想在stack中子组件超出父组件的约束 只需要stack的 clipBehavior属性设置 Clip.none
完整代码
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as vector;
class BottomBar extends StatefulWidget {
final Color? iconBackgroundColor;
final Color? backgroundColor;
final List<TabItem> tabItem;
final Function onBarTap;
final int? animationMilliseconds;
const BottomBar(
{Key? key,
this.iconBackgroundColor = Colors.deepPurple,
required this.tabItem,
required this.onBarTap,
this.animationMilliseconds = 300,
this.backgroundColor = Colors.white})
: super(key: key);
@override
State<BottomBar> createState() => _BottomBarState();
}
class _BottomBarState extends State<BottomBar> with TickerProviderStateMixin {
late AnimationController _animationController;
late Tween<double> _positionTween;
late Animation<double> _positionAnimation;
late AnimationController _fadeOutController;
late Animation<double> _fadeFabOutAnimation;
late Animation<double> _fadeFabInAnimation;
double fabIconAlpha = 1;
///是1的话在中间
int barIndex = 1;
@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: widget.animationMilliseconds!));
_fadeOutController =
AnimationController(vsync: this, duration: Duration(milliseconds: (widget.animationMilliseconds! ~/ 5)));
_positionTween = Tween<double>(begin: 0, end: 0);
_positionAnimation = _positionTween.animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOut))
..addListener(() {
setState(() {});
});
_fadeFabOutAnimation =
Tween<double>(begin: 1, end: 0).animate(CurvedAnimation(parent: _fadeOutController, curve: Curves.easeOut))
..addListener(() {
setState(() {
fabIconAlpha = _fadeFabOutAnimation.value;
});
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
setState(() {});
}
});
_fadeFabInAnimation = Tween<double>(begin: 0, end: 1)
.animate(CurvedAnimation(parent: _animationController, curve: const Interval(0.8, 1, curve: Curves.easeOut)))
..addListener(() {
setState(() {
fabIconAlpha = _fadeFabInAnimation.value;
});
});
}
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
alignment: Alignment.topCenter,
children: <Widget>[
Container(
height: 65,
padding: const EdgeInsets.only(bottom: 18),
decoration: BoxDecoration(
color: widget.backgroundColor,
boxShadow: const [BoxShadow(color: Colors.black12, offset: Offset(0, -1), blurRadius: 8)]),
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildBarItems(),
),
),
Positioned(
top: -45,
left: 0,
right: 0,
child: IgnorePointer(
child: Container(
decoration: const BoxDecoration(color: Colors.transparent),
child: Align(
heightFactor: 0.5,
alignment: Alignment(_positionAnimation.value, -1),//_positionAnimation.value控制这个叠叠的圆的位置,根据点的导航修改圆的x轴的位置
child: FractionallySizedBox(
widthFactor: 1/3,//控制占的宽度
child: Stack(
alignment: Alignment.center,
children: <Widget>[
SizedBox(
height: 90,
width: 90,
child: ClipRect(
clipper: HalfClipper(),//裁剪
child: Center(
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: widget.backgroundColor,
shape: BoxShape.circle,
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 8)])),
)),
),
SizedBox(
height: 60,
width: 60,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.iconBackgroundColor,
border: Border.all(color: Colors.white, width: 5, style: BorderStyle.none)),
child: Padding(
padding: const EdgeInsets.all(0.0),
child: Opacity(
opacity: fabIconAlpha,//白色图标切换时的动画
child: Icon(
widget.tabItem[barIndex].iconData,
color: Colors.white,
),
),
),
),
)
],
),
),
),
),
),
),
],
);
}
List<Widget> _buildBarItems() {
List<Widget> barItems = [];
for (int i = 0; i < widget.tabItem.length; i++) {
barItems.add(Expanded(
child: Stack(
fit: StackFit.expand,
children: [
SizedBox(
height: double.infinity,
width: double.infinity,
child: AnimatedAlign(
// curve: Curves.easeIn,//动画曲线
duration: Duration(milliseconds: widget.animationMilliseconds!),
alignment: Alignment(0, (barIndex == i)? 3 : 8),//控制文字的位置
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
widget.tabItem[i].title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
)),
),
SizedBox(
height: double.infinity,
width: double.infinity,
child:AnimatedOpacity(
duration: Duration(milliseconds: widget.animationMilliseconds!),//动画时长
opacity: (barIndex == i) ? 0 : 1,////控制icon的透明度
child: IconButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: const EdgeInsets.all(0),
alignment: const Alignment(0, 0),
icon: Icon(
widget.tabItem[i].iconData,
color: widget.iconBackgroundColor,
),
onPressed: () {
setState(() {
barIndex = i;
widget.onBarTap(barIndex);
_positionTween.begin = _positionAnimation.value; //圆开始位置
_positionTween.end = i - 1; //圆结束的位置
_animationController.reset();
_fadeOutController.reset();
_animationController.forward();
_fadeOutController.forward();
});
},
),
),
)
],
),
));
}
return barItems;
}
}
//裁剪成一半
class HalfClipper extends CustomClipper<Rect> {
@override
Rect getClip(Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height / 2);
return rect;
}
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) {
return true;
}
}
class TabItem {
final String title;
final IconData iconData;
TabItem({
required this.iconData,
required this.title,
});
}