Flutter——实现网易云音乐的Tabbar切换效果

49 篇文章 0 订阅
47 篇文章 0 订阅

介绍

预览图

分析

效果非常简单,在切换的时候,对应的文字要缩小/放大。

我们来实现这个自定义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

内部搜索即可

Demo地址 - github

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Flutter 中,你可以使用 `CupertinoTabBar` 和 `CupertinoTabScaffold` 来实现带有动画的 TabBar 切换效果。下面是一个简单的示例代码: ```dart import 'package:flutter/cupertino.dart'; class MyTabScreen extends StatefulWidget { @override _MyTabScreenState createState() => _MyTabScreenState(); } class _MyTabScreenState extends State<MyTabScreen> { int _currentIndex = 0; @override Widget build(BuildContext context) { return CupertinoTabScaffold( tabBar: CupertinoTabBar( items: [ BottomNavigationBarItem( icon: Icon(CupertinoIcons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(CupertinoIcons.search), label: 'Search', ), BottomNavigationBarItem( icon: Icon(CupertinoIcons.person), label: 'Profile', ), ], currentIndex: _currentIndex, onTap: (index) { setState(() { _currentIndex = index; }); }, ), tabBuilder: (BuildContext context, int index) { return CupertinoTabView( builder: (BuildContext context) { switch (index) { case 0: return HomeScreen(); case 1: return SearchScreen(); case 2: return ProfileScreen(); default: return Container(); } }, ); }, ); } } class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: Text('Home'), ), child: Center( child: Text('Home Screen'), ), ); } } class SearchScreen extends StatelessWidget { @override Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: Text('Search'), ), child: Center( child: Text('Search Screen'), ), ); } } class ProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: Text('Profile'), ), child: Center( child: Text('Profile Screen'), ), ); } } ``` 在这个示例中,我们使用 `CupertinoTabScaffold` 和 `CupertinoTabBar` 来创建底部的 TabBar,然后在 `tabBuilder` 方法中根据索引值切换显示不同的页面。在 `onTap` 回调中,我们可以更新 `_currentIndex` 的值来实现切换 Tab 时的动画效果。 以上是一个基本的示例,你可以根据自己的需求进行定制和修改。希望对你有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值