Flutter自定義BottomNavigatorBar實現(xiàn)首頁導(dǎo)航

前言

底部導(dǎo)航筛谚,對于做app的同學(xué)再熟悉不過了,不過有時候設(shè)計為了突出別樣的風(fēng)格停忿,就會來點不一樣的驾讲,比如下面的效果圖。我們使用系統(tǒng)提供的控件席赂,是無法實現(xiàn)的吮铭。在看了系統(tǒng)提供的源碼后,發(fā)現(xiàn)也不是太復(fù)雜颅停,于是乎谓晌,自己動手實現(xiàn)一個,下面的代碼復(fù)制過去癞揉,改改資源文件是直接可以用的纸肉。

效果圖

自定義底部導(dǎo)航

定義我們需要的主題


class BottomBarItem {
  BottomBarItem(
      {required this.icon,
      required this.activeIcon,
      required this.title,
      this.activeColor = Colors.blue,
      this.inactiveColor,
      this.textAlign = TextAlign.center,
      this.singleIcon = false});

  final Widget icon;///未選中的狀態(tài)
  final Widget activeIcon;///選中的狀態(tài)
  final Widget title;///標(biāo)題
  final Color activeColor;///選中的顏色
  final Color? inactiveColor;///未選中的顏色
  final TextAlign textAlign;
  final bool singleIcon;///是否只包含icon
}

定義BottomNavigatorBar

///作者  : Pig Huitao
///時間  : 2022/1/6
///郵箱  : pig.huitao@gmail.com
class BottomNavigatorBar extends StatelessWidget {
  const BottomNavigatorBar(
      {Key? key,
      this.selectedIndex = 0,
      this.iconSize = 24,
      this.backgroundColor,
      this.showElevation = true,
      this.animationDuration = const Duration(milliseconds: 270),
      required this.items,
      required this.onItemSelected,
      this.mainAxisAlignment = MainAxisAlignment.spaceBetween,
      this.itemCornerRadius = 50,
      this.containerHeight = 56,
      this.curve = Curves.linear,
      this.singleIcon = false})
      : super(key: key);

  final int selectedIndex;
  final double iconSize;
  final Color? backgroundColor;
  final bool showElevation;
  final Duration animationDuration;
  final List<BottomBarItem> items;
  final ValueChanged<int> onItemSelected;
  final MainAxisAlignment mainAxisAlignment;
  final double itemCornerRadius;
  final double containerHeight;
  final Curve curve;
  final bool singleIcon;

  @override
  Widget build(BuildContext context) {
    final bgColor = backgroundColor ?? Theme.of(context).bottomAppBarColor;
    return Container(
      decoration: BoxDecoration(
        color: bgColor,
        boxShadow: [
          if (showElevation)
            const BoxShadow(
              color: Colors.black12,
              blurRadius: 2,
            )
        ],
      ),
      child: SafeArea(
        child: Container(
          width: double.infinity,
          height: containerHeight,
          alignment: Alignment.center,
          padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 20),
          child: Row(
            mainAxisAlignment: mainAxisAlignment,
            children: items.map((e) {
              var index = items.indexOf(e);
              return GestureDetector(
                onTap: () => onItemSelected(index),
                child: _ItemWidget(
                  iconSize: iconSize,
                  isSelected: index == selectedIndex,
                  item: e,
                  backgroundColor: bgColor,
                  itemCornerRadius: itemCornerRadius,
                  animationDuration: animationDuration,
                  curve: curve,
                ),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}

最后的ImteWidget


class _ItemWidget extends StatelessWidget {
  const _ItemWidget(
      {Key? key,
      required this.iconSize,
      required this.isSelected,
      required this.item,
      required this.backgroundColor,
      required this.itemCornerRadius,
      required this.animationDuration,
      this.curve = Curves.linear,
      this.singIcon = false})
      : super(key: key);

  final double iconSize;
  final bool isSelected;
  final BottomBarItem item;
  final Color backgroundColor;
  final double itemCornerRadius;
  final Duration animationDuration;
  final Curve curve;
  final bool singIcon;

  @override
  Widget build(BuildContext context) {
    return Semantics(
      container: true,
      selected: isSelected,
      child: AnimatedContainer(
        height: double.maxFinite,
        duration: animationDuration,
        curve: curve,
        decoration: BoxDecoration(
          color: backgroundColor,
          borderRadius: BorderRadius.circular(itemCornerRadius),
        ),
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          physics: NeverScrollableScrollPhysics(),
          child: Container(
              padding: EdgeInsets.symmetric(horizontal: 8),
              alignment: Alignment.center,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  _buildIcon(isSelected, item),
                  const SizedBox(
                    height: 2,
                  ),
                  if (!item.singleIcon)
                    Expanded(
                        child: Container(
                      padding: EdgeInsets.symmetric(horizontal: 4),
                      child: DefaultTextStyle.merge(
                          child: item.title,
                          style: _textStyle(isSelected, item)),
                    ))
                ],
              )),
        ),
      ),
    );
  }

  TextStyle _textStyle(bool isSelected, BottomBarItem item) {
    ///返回文字樣式
    if (isSelected) {
      return TextStyle(
          color: item.activeColor, fontSize: 12);
    } else {
      return TextStyle(
          color: item.inactiveColor,
          fontSize: 12);
    }
  }

  Widget _buildIcon(bool isSelected, BottomBarItem item) {
    ///根據(jù)選中的state,返回不同的icon
    if (isSelected) {
      return Expanded(child: item.activeIcon);
    } else {
      return Expanded(child: item.icon);
    }
  }
}

完整的BottomNavigatorBar

///作者  : Pig Huitao
///時間  : 2022/1/6
///郵箱  : pig.huitao@gmail.com
class BottomNavigatorBar extends StatelessWidget {
  const BottomNavigatorBar(
      {Key? key,
      this.selectedIndex = 0,
      this.iconSize = 24,
      this.backgroundColor,
      this.showElevation = true,
      this.animationDuration = const Duration(milliseconds: 270),
      required this.items,
      required this.onItemSelected,
      this.mainAxisAlignment = MainAxisAlignment.spaceBetween,
      this.itemCornerRadius = 50,
      this.containerHeight = 56,
      this.curve = Curves.linear,
      this.singleIcon = false})
      : super(key: key);

  final int selectedIndex;
  final double iconSize;
  final Color? backgroundColor;
  final bool showElevation;
  final Duration animationDuration;
  final List<BottomBarItem> items;
  final ValueChanged<int> onItemSelected;
  final MainAxisAlignment mainAxisAlignment;
  final double itemCornerRadius;
  final double containerHeight;
  final Curve curve;
  final bool singleIcon;

  @override
  Widget build(BuildContext context) {
    final bgColor = backgroundColor ?? Theme.of(context).bottomAppBarColor;
    return Container(
      decoration: BoxDecoration(
        color: bgColor,
        boxShadow: [
          if (showElevation)
            const BoxShadow(
              color: Colors.black12,
              blurRadius: 2,
            )
        ],
      ),
      child: SafeArea(
        child: Container(
          width: double.infinity,
          height: containerHeight,
          alignment: Alignment.center,
          padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 20),
          child: Row(
            mainAxisAlignment: mainAxisAlignment,
            children: items.map((e) {
              var index = items.indexOf(e);
              return GestureDetector(
                onTap: () => onItemSelected(index),
                child: _ItemWidget(
                  iconSize: iconSize,
                  isSelected: index == selectedIndex,
                  item: e,
                  backgroundColor: bgColor,
                  itemCornerRadius: itemCornerRadius,
                  animationDuration: animationDuration,
                  curve: curve,
                ),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}

class _ItemWidget extends StatelessWidget {
  const _ItemWidget(
      {Key? key,
      required this.iconSize,
      required this.isSelected,
      required this.item,
      required this.backgroundColor,
      required this.itemCornerRadius,
      required this.animationDuration,
      this.curve = Curves.linear,
      this.singIcon = false})
      : super(key: key);

  final double iconSize;
  final bool isSelected;
  final BottomBarItem item;
  final Color backgroundColor;
  final double itemCornerRadius;
  final Duration animationDuration;
  final Curve curve;
  final bool singIcon;

  @override
  Widget build(BuildContext context) {
    return Semantics(
      container: true,
      selected: isSelected,
      child: AnimatedContainer(
        height: double.maxFinite,
        duration: animationDuration,
        curve: curve,
        decoration: BoxDecoration(
          color: backgroundColor,
          borderRadius: BorderRadius.circular(itemCornerRadius),
        ),
        child: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          physics: NeverScrollableScrollPhysics(),
          child: Container(
              padding: EdgeInsets.symmetric(horizontal: 8),
              alignment: Alignment.center,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  _buildIcon(isSelected, item),
                  const SizedBox(
                    height: 2,
                  ),
                  if (!item.singleIcon)
                    Expanded(
                        child: Container(
                      padding: EdgeInsets.symmetric(horizontal: 4),
                      child: DefaultTextStyle.merge(
                          child: item.title,
                          style: _textStyle(isSelected, item)),
                    ))
                ],
              )),
        ),
      ),
    );
  }

  TextStyle _textStyle(bool isSelected, BottomBarItem item) {
    ///返回文字樣式
    if (isSelected) {
      return TextStyle(
          color: item.activeColor, fontSize: 12);
    } else {
      return TextStyle(
          color: item.inactiveColor,
          fontSize: 12);
    }
  }

  Widget _buildIcon(bool isSelected, BottomBarItem item) {
    ///根據(jù)選中的state喊熟,返回不同的icon
    if (isSelected) {
      return Expanded(child: item.activeIcon);
    } else {
      return Expanded(child: item.icon);
    }
  }
}

class BottomBarItem {
  BottomBarItem(
      {required this.icon,
      required this.activeIcon,
      required this.title,
      this.activeColor = Colors.blue,
      this.inactiveColor,
      this.textAlign = TextAlign.center,
      this.singleIcon = false});

  final Widget icon;///未選中的狀態(tài)
  final Widget activeIcon;///選中的狀態(tài)
  final Widget title;///標(biāo)題
  final Color activeColor;///選中的顏色
  final Color? inactiveColor;///未選中的顏色
  final TextAlign textAlign;
  final bool singleIcon;///是否只包含icon
}

使用

///作者  : Pig Huitao
///時間  : 2022/1/5
///郵箱  : pig.huitao@gmail.com

class MainPage extends StatefulWidget {
const  MainPage({Key? key}):super(key: key);
  @override
  _MainPage createState() => _MainPage();
}

class _MainPage extends State<MainPage>
    with SingleTickerProviderStateMixin {
  int _currentIndex = 0;
  final _inactiveColor = Colors.grey;
  final _activeColor = Colors.blue;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('自定義底部導(dǎo)航'),
      ),
      body: getBody(),
      bottomNavigationBar: _buildBottomBar(),
    );
  }

  Widget _buildBottomBar() {
    return BottomNavigatorBar(
      containerHeight: 55,
      backgroundColor: Colors.white,
      selectedIndex: _currentIndex,
      showElevation: true,
      itemCornerRadius: 24,
      curve: Curves.easeIn,
      onItemSelected: (index) => setState(() => _currentIndex = index),
      items: <BottomBarItem>[
        BottomBarItem(
          icon: const Image(
            image: AssetImage('assets/images/icon_home_unselected.png'),
            fit: BoxFit.fill,
          ),
          activeIcon: const Image(
            image: AssetImage('assets/images/icon_home_selected.png'),
            fit: BoxFit.fill,
          ),
          title: const Text('首頁'),
          activeColor: Colors.blue,
          inactiveColor: _inactiveColor,
          textAlign: TextAlign.center,
        ),
        BottomBarItem(
          icon: const Image(
            image: AssetImage('assets/images/icon_circle_unselected.png'),
            fit: BoxFit.fill,
          ),
          activeIcon: const Image(
            image: AssetImage('assets/images/icon_circle_selected.png'),
            fit: BoxFit.fill,
          ),
          title: const Text('圈子'),
          activeColor: _activeColor,
          inactiveColor: _inactiveColor,
          textAlign: TextAlign.center,
        ),
        BottomBarItem(
            icon: const Image(
              width: 60,
              height: 38,
              image: AssetImage('assets/images/icon_publish.png'),
              fit: BoxFit.fill,
            ),
            activeIcon: const Image(
              width: 60,
              height: 38,
              image: AssetImage('assets/images/icon_publish.png'),
              fit: BoxFit.fill,
            ),
            title: const Text(
              '發(fā)布 ',
            ),
            activeColor: _activeColor,
            inactiveColor: _inactiveColor,
            textAlign: TextAlign.center,
            singleIcon: true),
        BottomBarItem(
          icon: const Image(
            image: AssetImage('assets/images/icon_message_unselected.png'),
            fit: BoxFit.fill,
          ),
          activeIcon: const Image(
            image: AssetImage('assets/images/icon_message_selected.png'),
            fit: BoxFit.fill,
          ),
          title: const Text('消息'),
          activeColor: _activeColor,
          inactiveColor: _inactiveColor,
          textAlign: TextAlign.center,
        ),
        BottomBarItem(
          icon: const Image(
            image: AssetImage("assets/images/icon_me_unselected.png"),
            fit: BoxFit.fill,
          ),
          activeIcon: const Image(
            image: AssetImage("assets/images/icon_me_selected.png"),
            fit: BoxFit.fill,
          ),
          title: const Text('我的'),
          activeColor: Colors.blue,
          inactiveColor: _inactiveColor,
          textAlign: TextAlign.center,
        ),
      ],
    );
  }

  Widget getBody() {
    List<Widget> pages = [
      Container(
        alignment: Alignment.center,
        child: const Text(
          "Home",
          style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
        ),
      ),
      Container(
        alignment: Alignment.center,
        child: const Text(
          "Circles",
          style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
        ),
      ),
      Container(
        alignment: Alignment.center,
        child: const Text(
          "Publishes",
          style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
        ),
      ),
      Container(
        alignment: Alignment.center,
        child: const Text(
          "Messages",
          style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
        ),
      ),
      Container(
        alignment: Alignment.center,
        child: const Text(
          "Users",
          style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
        ),
      ),
    ];
    return IndexedStack(
      index: _currentIndex,
      children: pages,
    );
  }
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末毁靶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子逊移,更是在濱河造成了極大的恐慌,老刑警劉巖龙填,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胳泉,死亡現(xiàn)場離奇詭異,居然都是意外死亡岩遗,警方通過查閱死者的電腦和手機扇商,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宿礁,“玉大人案铺,你說我怎么就攤上這事“鹁福” “怎么了控汉?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長返吻。 經(jīng)常有香客問我姑子,道長,這世上最難降的妖魔是什么测僵? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任街佑,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘沐旨。我一直安慰自己森逮,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布磁携。 她就那樣靜靜地躺著褒侧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪颜武。 梳的紋絲不亂的頭發(fā)上璃搜,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音鳞上,去河邊找鬼这吻。 笑死,一個胖子當(dāng)著我的面吹牛篙议,可吹牛的內(nèi)容都是我干的唾糯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼鬼贱,長吁一口氣:“原來是場噩夢啊……” “哼移怯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起这难,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤舟误,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后姻乓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嵌溢,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年蹋岩,在試婚紗的時候發(fā)現(xiàn)自己被綠了赖草。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡剪个,死狀恐怖秧骑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情扣囊,我是刑警寧澤乎折,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站如暖,受9級特大地震影響笆檀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜盒至,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一酗洒、第九天 我趴在偏房一處隱蔽的房頂上張望士修。 院中可真熱鬧,春花似錦樱衷、人聲如沸棋嘲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沸移。三九已至,卻和暖如春侄榴,著一層夾襖步出監(jiān)牢的瞬間雹锣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工癞蚕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蕊爵,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓桦山,卻偏偏與公主長得像攒射,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子恒水,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容