介绍
预览图
分析
效果非常简单,在切换的时候,对应的文字要缩小/放大。
我们来实现这个自定义tabbar
实现
首先我们定义一个类
class CustomTabBar extends WidgetState with SingleTickerProviderStateMixin
因为动画的原因,所以需要混入
SingleTickerProviderStateMixin
CustomTabBar
首先需要定义两个回调
//因为我用到的MVVM,所以需要将tabbar的vm传出,方便外层控制tabbar
typedef TabBarController = void Function(TabBarViewModel controller);
//当我们点击tab的item的时候,需要将对应index传出 ,外层可以切换pageView
typedef TabClick = void Function(int index);
两个变量用于控制文字放大的系数阈值
final double min = 1.0;
final double max = 1.2;
///动画
AnimationController controller;
Animation animation;
接下来我们看一下变量的初始化
@override
void initState() {
super.initState();
//动画很快 只有50ms
controller = AnimationController(duration: Duration(milliseconds: 50),vsync: this);
//动画控制文字的放大和缩小
animation = Tween<double>(begin: min,end: max).animate(controller);
controller.addListener(() {
//对动画进行监听,
//并调用updateFactor()方法
if(!parentVM.isResetting){
parentVM.updateFactor(animation.value);
}
});
controller.addStatusListener((status) {
if(status == AnimationStatus.completed){
//当动画执行完毕后,我们重置动画,这里的重置使我们自己的方法
//而非直接调用controller的
parentVM.resetController();
}
});
}
我们先来看一下 updateFactor()和resetController()方法
//这个变量用于字体的放大和缩小
double textScaleFactor = 1.2;
updateFactor(double newV){
//我们将动画的value传进来更新textScaleFactor
//下面的表达式,可以确保 这个放大系数 在1-1.2之间
textScaleFactor = newV > textScaleFactor ? newV.clamp(1.0, 1.2) : textScaleFactor;
notifyListeners();
}
//这里用于界定controller的reset,
//避免controller reset时,缩小了字体,所以加此变量
bool isResetting = false;
void resetController(){
//从上面的代码可以看到,(监听动画那部分)
//reset=true的时候,将不会触发页面的刷新
isResetting = true;
controller.reset();
//重置完成后将状态置为false
isResetting = false;
}
接下来我们看一下布局
return Container(
color: Colors.white,
height: getWidthPx(80),
child: buildTab(),
);
static const double txtSize = 36;
Widget buildTab() {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
wrap(TabBarItem(parentVM, '我的', 0,textSize: getSp(txtSize)).generateWidget(),0),
wrap(TabBarItem(parentVM, '发现', 1,textSize: getSp(txtSize)).generateWidget(),1),
wrap(TabBarItem(parentVM, '云村', 2,textSize: getSp(txtSize)).generateWidget(),2),
wrap(TabBarItem(parentVM, '视频', 3,textSize: getSp(txtSize)).generateWidget(),3),
],
);
}
Widget wrap(Widget child,int index){
return GestureDetector(
onTap: (){
tabClick(index);
},
child: Container(
alignment: Alignment.bottomCenter,
width: getWidthPx(110),
child: child,
),
);
}
代码很简单,基本的4个tab item 横向布局,这里的item是我们自定义的,将在后面介绍
我们主要看一下这个方法,他将会触发动画
tabClick(index);//这个就是我们的回调,最终会将item的index传到外层页面
我们看一下外层页面的动画触发
页面的动画触发
下方代码,是页面的布局,这里简称homePage,这个自定义tab 会与一个pageview绑定
buildTab() {
return Container(
height: getWidthPx(80),
//color: Colors.greenAccent,
child: CustomTabBar((controller){
tabController = controller;
},(index){
//tab click
pageController.jumpToPage(index);
}).generateWidget(),
);
}
///pageview的 代码(删减版)
PageView(
controller: pageController,
onPageChanged: (index){
tabController.switchPage(index);
pageIndex = index;
},
...)
我们按照上节的回调来过一遍流程,当回调触发的时候,将会发出pageview的切换
(index){
//tab click
pageController.jumpToPage(index);
}
而pageview切换完成后,又会触发它自己的回调
onPageChanged: (index){
//page view的回调又触发了 tab的switchPage(index)方法
tabController.switchPage(index);
pageIndex = index;
}
还记得tabcontroller吗? 它实际是自定义tab的 VM可以,我们来看一下它的switchPage(index)方法
switchPage(int index){
//我们将tab的 index配置为和 pageview 相匹配
pageIndex = index;
//下面
record();
//刷新一下界面
notifyListeners();
//执行放大动画
controller.forward();
}
//这个方法我们是用来记录tab 的 item index
//因为目标的index要放大,而前一个item则要缩小,
//它相当于一个切换历史记录
//始终只记录两个值
List<int> indexRecord = [];
void record() {
if(indexRecord.length == 3) indexRecord.removeAt(0);
indexRecord.add(pageIndex);
}
这个pageindex和对应的历史记录联合起来,就可以控制item的缩小和放大了,我们看一下item的实现.
tabbar item
说明我将写在注释里
class TabBarItem extends WidgetState {
//外层的vm,这个vm获取的方法很多,我这个构造函数传参方便
final TabBarViewModel parentVM;
final String text;//文案
final double textSize;//字体大小
final index;//每个item的 标识 一般是 0,1,2,3
TabBarItem(this.parentVM,this.text,this.index,{this.textSize = 20})
:assert(parentVM!=null),assert(text.isNotEmpty);
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
//这里的表达式比较绕
//简单讲,它是根据咱们上面讲的 pageIndex和 recordList历史记录
//来获得当前的currentIndex和 上一个 preIndex
// currentIndex 我们会放大
// preIndex 我们会对应缩小
return Text(text,
//我们通过textScaleFactor来对 字体进行放大
textScaleFactor:(index == parentVM.pageIndex
?parentVM.textScaleFactor :
(index == parentVM.getLastIndex())
? parentVM.textScaleFactor : parentVM.min) ,
style: TextStyle(fontSize: textSize,
color: index == parentVM.pageIndex?Colors.black:Colors.grey),);
}
}
至此整个功能就开发完毕了,谢谢大家阅览
如有不足之处,欢迎指出 :)
Demo
内部搜索即可