底部工具列 BottomAppBar


在認識 BottomNavigationBar 的同時,往往都會看到 BottomAppBar,不少文件會說,如果 BottomNavigationBar 不能滿足你,想自訂個底部工具列,可以使用 BottomAppBar,這麼說某些程度上也沒錯啦!

例如,來模仿一下 BottomNavigationBar,在 bottom_nav_bar.dart 中實作個 BottomNavnBar

import 'package:flutter/material.dart';

class BottomNavBar extends StatefulWidget {
  final items;
  final Function onTap;

  BottomNavBar({this.items, this.onTap});

  @override
  State<StatefulWidget> createState() => _BottomNavBar();
}

class _BottomNavBar extends State<BottomNavBar> {
  int selectedIdx = 0;

  @override
  Widget build(BuildContext context) {
    final barItems = List<Widget>();
    for(var i = 0; i < widget.items.length; i++) {
      barItems.add(Column(
        // 依元件決定最小高度
        mainAxisSize: MainAxisSize.min,
        // 從底部排列
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          IconButton(
            // 依選中與否,從父裔中取得佈景主題的顏色
            color: selectedIdx == i ? Theme.of(context).accentIconTheme.color : Colors.black,
            icon: widget.items[i].icon,
            onPressed: () {
              setState(() {
                selectedIdx = i;
                widget.onTap(i);
              });
            },
          ),
          DefaultTextStyle(
            style: TextStyle(
              // 依選中與否,從父裔中取得佈景主題的顏色
              color: selectedIdx == i ? Theme.of(context).accentIconTheme.color : Colors.black,
            ),
            child: widget.items[i].title,
          )
        ],
      ));
    }

    return BottomAppBar(
      color: Theme.of(context).accentColor,
      child: Row(
        // 平均分配空間
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: barItems,
      )
    );
  }
}

class BottomNavBarItem {
  Widget title;
  Icon icon;
  BottomNavBarItem({this.title, this.icon});
}

這麼一來,就可以將〈底部導覽列 BottomNavigationBar〉中的 BottomNavigationBarBottomNavBar 取代:

import 'package:flutter/material.dart';
import 'bottom_nav_bar.dart';

void main() => runApp(
  MaterialApp(
    home: Home()
  )
);

class Home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final books = [
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/ACL059300.jpg',
      name: 'Java SE 14 技術手冊',
    ),
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/ACL054400.jpg',
      name: 'Python 3.7 技術手冊',
    ),
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/AEL022800.jpg',
      name: 'JavaScript 技術手冊',
    )
  ];

  var bookIdx;

  @override
  void initState() {
    bookIdx = 0;
    super.initState();
  }

  void page(idx) {
    setState(() {
      bookIdx = idx;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: books[bookIdx],
      bottomNavigationBar: BottomNavBar(
        items: [
          BottomNavBarItem(title: Text('Java'), icon: Icon(Icons.local_cafe)),
          BottomNavBarItem(title: Text('Python'), icon: Icon(Icons.local_grocery_store)),
          BottomNavBarItem(title: Text('JavaScript'), icon: Icon(Icons.local_pizza)),
        ],
        onTap: page,
      ),
    );
  }
}

class Book extends StatelessWidget {
  final String imgSrc;
  final String name;

  Book({this.imgSrc, this.name});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Center(
          child: Image.network(imgSrc),
        ),
        Text(name)
      ],
    );
  }
}

執行結果是差不多的:

底部工具列 BottomAppBar

只不過,幹嘛還包個 BottomAppBar?直接將 _BottomNavBarbuild 裡的 Row 指定給 ScaffoldbottomNavigationBar 不就好了?沒錯!如果只是要自訂底部工具列,根本就不需要 BottomAppBar,方才只是純綷想玩,模仿一下 BottomNavigationBar 罷了。

其實 API 文件中寫到,BottomAppBar 主要是為了搭配 ScaffoldfloatingActionButton 特性,如果 floatingActionButton 蓋到 BottomAppBar,你可以提供一個缺口的繪製效果。例如,在 _BottomNavBarbuild 做點修改:

import 'package:flutter/material.dart';

...

class _BottomNavBar extends State<BottomNavBar> {
  int selectedIdx = 0;

  @override
  Widget build(BuildContext context) {
    ...略

    return BottomAppBar(
      color: Theme.of(context).accentColor,
      // 設定缺口形狀
      shape: CircularNotchedRectangle(),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: barItems,
      )
    );
  }
}

...略

然後,設定一下 ScaffoldfloatingActionButtonfloatingActionButtonLocation

import 'package:flutter/material.dart';
import 'bottom_nav_bar.dart';

void main() => runApp(
    MaterialApp(
        home: Home()
    )
);

class Home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final books = [
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/ACL059300.jpg',
      name: 'Java SE 14 技術手冊',
    ),
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/ACL054400.jpg',
      name: 'Python 3.7 技術手冊',
    ),
  ];

  var bookIdx;

  ...略

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 設定 FloatingActionButton
      floatingActionButton: FloatingActionButton(
          child: Icon(
            Icons.add,
            color: Colors.white,
          )),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

      body: books[bookIdx],
      bottomNavigationBar: BottomNavBar(
        items: [
          BottomNavBarItem(title: Text('Java'), icon: Icon(Icons.local_cafe)),
          BottomNavBarItem(title: Text('Python'), icon: Icon(Icons.local_grocery_store)),
        ],
        onTap: page,
      ),
    );
  }
}

class Book extends StatelessWidget {
  ...略
}

就會出現以下的效果了:

底部工具列 BottomAppBar

要注意的是,缺口效果是畫出來的,顏色是由 BottomAppBarcolor 決定,也就是說,若想要有缺口效果,自訂 BottomAppBar 時,就不能設定其 child 元件的顏色,例如底下可以呈現缺口:

import 'package:flutter/material.dart';
import 'bottom_nav_bar.dart';

void main() => runApp(
  MaterialApp(
    home: Scaffold(
      floatingActionButton: FloatingActionButton(
          child: Icon(
            Icons.add,
            color: Colors.white,
          )),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      bottomNavigationBar: BottomAppBar(
        color: Colors.blue,
        shape: CircularNotchedRectangle(),
        child: Container(
          height: kBottomNavigationBarHeight,
        )
      ),
    )
  )
);

執行結果如下:

底部工具列 BottomAppBar

然而,設定了 Containercolor 後,就不會有缺口了:

import 'package:flutter/material.dart';
import 'bottom_nav_bar.dart';

void main() => runApp(
  MaterialApp(
    home: Scaffold(
      floatingActionButton: FloatingActionButton(
          child: Icon(
            Icons.add,
            color: Colors.white,
          )),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      bottomNavigationBar: BottomAppBar(
        color: Colors.blue,
        shape: CircularNotchedRectangle(),
        child: Container(
          color: Colors.green,  // 設定了顏色
          height: kBottomNavigationBarHeight,
        )
      ),
    )
  )
);

這是因為缺口畫完後,就又被 Container 的顏色繪製給覆蓋過去了:

底部工具列 BottomAppBar

因此正確來說,BottomAppBar 是給你自訂有缺口的工具列,而且是專門為了搭配 ScaffoldfloatingActionButton 特性。

那麼可不可以提供按鈕處凸起而不是缺口呢?BottomAppBarshape 接受的 NotchedShape,顧名思義,它應該就是用來做缺口用的,不過,NotchedShape 主要是提供一個 Path

abstract class NotchedShape {
  const NotchedShape();
  Path getOuterPath(Rect host, Rect guest);
}

也就是說,提供一個形狀的路徑資訊,CircularNotchedRectangle 提供的應就是個與矩形計算後,具有缺口的矩形路徑資訊,然後再用來繪圖,因此如果有心研究一下 BottomAppBar 是怎麼利用 NotchedShape 繪圖的話,是可以做出按鈕處凸起的效果,網路上有一些看來不錯的專案,提供這類的效果,搜尋一下「convex bottom bar」應該就能找到。