ScaffoldのbottomNavigationBarに配置されることを想定したラジオボタン形式のウィジェット。
基本的な内容、BottomNavigationBarType.shiftingの時の問題点、TabBarViewと連携できるのかなどなど調べてみた。
基本的内容
UI的にはアイコン、ラベル、ツールチップを子要素としてRow形式のラジオボタンといったもの。
良く使う使い方
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になってしまう。
原因
上記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はアニメーションのためのインターフェースがないので、その部分との連携ができないので、上のような表現が精一杯な感じになってしまった。
コメント