flutter自定义底部导航栏 开始

开始

想做个有意思的底部导航栏,不想用官方的,找了一些参考,发现这个比较有意思,还有动画。

github.com/tunitowen/t…

image.png

遗憾的但是拿过来这个并不能直接用

思路

在布局上来看 底部导航栏是一个横向的布局 我们用 Row 就可以了,每Bar切换是与页面绑定的,而且Bar的个数
是不固定的,并且每个Bar都要与对应的页面相绑定。

新建一个 Bar的类存储每个导航栏的信息

1
2
3
4
5
6
7
8
9
dart复制代码class TabItem {
final String title;
final IconData iconData;

TabItem({
required this.iconData,
required this.title,
});
}

当然你还可以扩充 比如设置这个加个颜色什么之类的 。

简单的底部导航栏

效果

4t4h9-f2mj6.gif

通过传入的导航,根据导航数量进行构建底部导航栏,barItems里添加每个导航的样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
dart复制代码
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 横向布局里即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
dart复制代码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所绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
dart复制代码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;
});
},
),
);
}

动画的底部导航栏

效果

ipw9c-o5lky.gif

通过传入的导航的数量构建底部导航栏还是大同小异,唯一的区别是,当选中的时候 icon变透明,以及title从底部移动上来的两个动画效果 使用了 AnimatedOpacity 控制透明度 AnimatedAlign 来控制标题的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
less复制代码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;
}

image.png

这个圆我们观察他!
大圆叠小圆 上面还有个icon

叠叠圆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
dart复制代码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

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
dart复制代码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,
});
}

ENDING 动画真好玩~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%