Flutter——實現(xiàn)網(wǎng)易云音樂的Tabbar切換效果

介紹

預(yù)覽圖

image

分析

效果非常簡單,在切換的時候,對應(yīng)的文字要縮小/放大。

我們來實現(xiàn)這個自定義tabbar

實現(xiàn)

首先我們定義一個類

class CustomTabBar extends WidgetState with SingleTickerProviderStateMixin

因為動畫的原因,所以需要混入

SingleTickerProviderStateMixin

CustomTabBar

首先需要定義兩個回調(diào)

//因為我用到的MVVM,所以需要將tabbar的vm傳出,方便外層控制tabbar
typedef TabBarController = void Function(TabBarViewModel controller);
//當(dāng)我們點擊tab的item的時候,需要將對應(yīng)index傳出 ,外層可以切換pageView
typedef TabClick = void Function(int index);

兩個變量用于控制文字放大的系數(shù)閾值

  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(() {
        //對動畫進行監(jiān)聽,
        //并調(diào)用updateFactor()方法
      if(!parentVM.isResetting){
        parentVM.updateFactor(animation.value);
      }

    });
    controller.addStatusListener((status) {
      if(status == AnimationStatus.completed){
        //當(dāng)動畫執(zhí)行完畢后,我們重置動畫,這里的重置使我們自己的方法
        //而非直接調(diào)用controller的
        parentVM.resetController();
      }
    });
  }

我們先來看一下 updateFactor()和resetController()方法

 //這個變量用于字體的放大和縮小
  double textScaleFactor = 1.2;
  updateFactor(double newV){
    //我們將動畫的value傳進來更新textScaleFactor
    //下面的表達式,可以確保 這個放大系數(shù) 在1-1.2之間
    textScaleFactor = newV > textScaleFactor ? newV.clamp(1.0, 1.2) : textScaleFactor;
    notifyListeners();
  }
    //這里用于界定controller的reset, 
    //避免controller reset時,縮小了字體,所以加此變量
  bool isResetting = false;
  void resetController(){
   //從上面的代碼可以看到,(監(jiān)聽動畫那部分)
   //reset=true的時候,將不會觸發(fā)頁面的刷新
    isResetting = true;
    controller.reset();
    //重置完成后將狀態(tài)置為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, '發(fā)現(xiàn)', 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是我們自定義的,將在后面介紹

我們主要看一下這個方法,他將會觸發(fā)動畫

tabClick(index);//這個就是我們的回調(diào),最終會將item的index傳到外層頁面

我們看一下外層頁面的動畫觸發(fā)

頁面的動畫觸發(fā)

下方代碼,是頁面的布局,這里簡稱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;
      },
      ...)
  

我們按照上節(jié)的回調(diào)來過一遍流程,當(dāng)回調(diào)觸發(fā)的時候,將會發(fā)出pageview的切換

    (index){
        //tab click
        pageController.jumpToPage(index);
      }

而pageview切換完成后,又會觸發(fā)它自己的回調(diào)

onPageChanged: (index){
        //page view的回調(diào)又觸發(fā)了 tab的switchPage(index)方法
        tabController.switchPage(index);
        pageIndex = index;
      }

還記得tabcontroller嗎? 它實際是自定義tab的 VM可以,我們來看一下它的switchPage(index)方法

  switchPage(int index){
   //我們將tab的 index配置為和 pageview 相匹配
    pageIndex = index;
    //下面
    record();
    //刷新一下界面
    notifyListeners();
    //執(zhí)行放大動畫
    controller.forward();
  }
  //這個方法我們是用來記錄tab 的 item index
  //因為目標(biāo)的index要放大,而前一個item則要縮小,
  //它相當(dāng)于一個切換歷史記錄
  //始終只記錄兩個值
  List<int> indexRecord = [];
    void record() {
    if(indexRecord.length == 3) indexRecord.removeAt(0);
    indexRecord.add(pageIndex);
  }
  

這個pageindex和對應(yīng)的歷史記錄聯(lián)合起來,就可以控制item的縮小和放大了,我們看一下item的實現(xiàn).

tabbar item

說明我將寫在注釋里

class TabBarItem extends WidgetState {
    //外層的vm,這個vm獲取的方法很多,我這個構(gòu)造函數(shù)傳參方便
  final TabBarViewModel parentVM;
  final String text;//文案
  final double textSize;//字體大小
  final index;//每個item的 標(biāo)識 一般是 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) {
   //這里的表達式比較繞
   //簡單講,它是根據(jù)咱們上面講的 pageIndex和 recordList歷史記錄
   //來獲得當(dāng)前的currentIndex和 上一個 preIndex
   // currentIndex 我們會放大
   // preIndex  我們會對應(yīng)縮小
    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),);
  }

}

至此整個功能就開發(fā)完畢了,謝謝大家閱覽

如有不足之處,歡迎指出 :)

Demo

內(nèi)部搜索即可

Demo地址 - github

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容