Flutter-BottomNavigationBar

ScaffoldのbottomNavigationBarに配置されることを想定したラジオボタン形式のウィジェット。

基本的な内容、BottomNavigationBarType.shiftingの時の問題点、TabBarViewと連携できるのかなどなど調べてみた。

基本的内容

UI的にはアイコン、ラベル、ツールチップを子要素としてRow形式のラジオボタンといったもの。

BottomNavigationBar class - material library - Dart API
API docs for the BottomNavigationBar class from the material library, for the Dart programming language.

良く使う使い方

PageViewと組み合わせて各々の要素に該当するPageを表示するという使い方になると思う。

その時のサンプル的な実装例が以下の様な感じになると思う。

class _BotNav4State extends State<BotNav4> {
  static const tabs = [
    BottomNavigationBarItem(label: "List", icon: Icon(Icons.list)),
    BottomNavigationBarItem(label: "Map", icon: Icon(Icons.map)),
    BottomNavigationBarItem(label: "alarm", icon: Icon(Icons.alarm)),
  ];

  late PageController _pageCtrl;
  int _selectedIndex = 0;

  @override
  void initState() {
    _pageCtrl = PageController(initialPage: _selectedIndex);
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text(
            "BotNav4 Test",
          ),
        ),
        body: PageView(
          controller: _pageCtrl,
          onPageChanged: (value) {
            setState(() {
              _selectedIndex = value;
            });
          },
          children: tabs.map((tab) {
            final String label = tab.label!.toLowerCase();
            return Center(
              child: Text(
                'This is the $label tab',
                style: const TextStyle(fontSize: 36),
              ),
            );
          }).toList(),
        ),
        bottomNavigationBar: BottomNavigationBar(
          onTap: (value) {
            setState(() {
              _pageCtrl.animateToPage(
                value,
                duration: kTabScrollDuration,
                curve: Curves.ease,
              );
              _selectedIndex = value;
            });
          },
          currentIndex: _selectedIndex,
          items: tabs,
        ));
  }

まず、ここで他と違うのがanimateToPageの引数部分。


durationとcurveの設定がないと、このようにページの切り替えが一瞬で終わるようになる。
ちなみにTabBarとTabBarViewの組み合わせでは、ページ切り替え時に左右スワイプ動作が行われる。


上記の様な動きにするため、durationとcurveの設定を行うのだけど、上の実装はTabBar(具体的にはTabController内)の実装に合わせたものになるので、動きを合わせる場合はこれに倣った方が良い。

BottomNavigationBarType.shifting時の問題点

この設定を行うと、色の設定をしていないと以下の様にアイコン・ラベルが見えなくなる。

設定がshiftingになるケースは、明示的にパラメータを設定する以外にitemsの子要素が4個以上の場合も同様にshiftingになってしまう。

原因

BottomNavigationBar items become white when more than 3 items are present · Issue #13642 · flutter/flutter
Steps to Reproduce When adding more than 3 items in a BottomNavigationBar, all items turn white, and are unreadable on t...

上記Issueにも書かれているのだけど、「BottomNavigationBar のアイテムがテキストとアイコンの場合、 DefaultTextStyle と IconTheme を介して白でレンダリングされます」とのこと。

実際の実装(_BottomNavigationBarStateの_createTilesメソッド内)を見てみると、fixedの場合、選択色はthemeData.colorScheme.primary(ライト)、themeData.colorScheme.secondary(ダーク)を使い、非選択色はthemeData.unselectedWidgetColorを使っている。
shiftingの場合、選択色・非選択色ともにthemeData.colorScheme.surfaceを使っている。

このためshiftingにするとバックグラウンド色と同系色となるので見えなく(見づらく)なってしまう結果になる。

対策

selectedItemColor、unselectedItemColorをfixedの時の色に合わせるという方法もあるのだけど、useLegacyColorSchemeをfalseにすることでfixedの時と同じ色になる。

補足1

Stackoverflowで「itemsを4個以上にすると見えなくなるんですけど」という質問に対しての答えは「fixedにしなさい」といった内容が占めてるんだけど、なんか違うような気がするな。

あとブログ系ではunselectedItemColorにdisabledColorを設定すると良いよというのがあるけど、disabledColorは選択不可の色指定なのでちょっと違うよな、というのもある。

補足2

TabBarをbottomNavigationBarに配置すると、BottomNavigationBarType.shiftingと同じ時のような状況になる。
こちらはきちんと色を設定してあげないといけない。

参考になる色は「選択色はthemeData.colorScheme.primary(ライト)、themeData.colorScheme.secondary(ダーク)を使い、非選択色はthemeData.unselectedWidgetColorを使っている。」という、BottomNavigationBarの実装になるかもしれない。

設定した場合、上の様な配色になる。

TabBarViewと連携できるか

通常View側はPageViewを使うのだけど、TabBarViewと連携できるのか調べてみた。

一応、以下の様な実装にすることで連携は可能。

class _BotNav3State extends State<BotNav3> with SingleTickerProviderStateMixin {
  static const tabs = <BottomNavigationBarItem>[
    BottomNavigationBarItem(label: "List", icon: Icon(Icons.list)),
    BottomNavigationBarItem(label: "Map", icon: Icon(Icons.map)),
    BottomNavigationBarItem(label: "alarm", icon: Icon(Icons.alarm)),
    BottomNavigationBarItem(label: "Settings", icon: Icon(Icons.settings)),
  ];

  late TabController _tabCtrl;

  @override
  void initState() {
    _tabCtrl = TabController(vsync: this, length: tabs.length);
    _tabCtrl.addListener(_setState);
    super.initState();
  }

  void _setState() {
    setState(() {});
  }

  @override
  void dispose() {
    _tabCtrl.removeListener(_setState);
    _tabCtrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text(
            "BotNav3 Test",
          ),
        ),
        body: TabBarView(
          controller: _tabCtrl,
          children: tabs.map((BottomNavigationBarItem tab) {
            final String label = tab.label!.toLowerCase();
            return Center(
              child: Text(
                'This is the $label tab',
                style: const TextStyle(fontSize: 36),
              ),
            );
          }).toList(),
        ),
        bottomNavigationBar: BottomNavigationBar(
          onTap: (value) {
            setState(() {
              _tabCtrl.animateTo(value);
            });
          },
          currentIndex: _tabCtrl.index,
          items: tabs,
        ));
  }
}

問題点があるとすると、TabBarView側をスワイプした時にBottomNavigationBarのindexが確定するタイミング。
ページ移動後の確定情報はChangeNotifierのリスナー経由になるのだけど、完全にページが移動後にこのイベントが発生するので、実際にBottomNavigationBarの更新されるタイミングが若干ずれる。

TabBar+TabBarViewでも同様にTabBar側の位置の確定タイミングは、ここで実装したBottomNavigationBarと同じなのだけど、TabBar側はデコレーション部分の移動アニメーションがあるので、スワイプ時と同時に移動していると感じることができている。

BottomNavigationBarはアニメーションのためのインターフェースがないので、その部分との連携ができないので、上のような表現が精一杯な感じになってしまった。

コメント

タイトルとURLをコピーしました