FlutterのTab
の中にIcon
やText
を使うと、タブの選択・非選択状態に応じて色がよしなに変わってくれます。しかし、カスタムWidgetやImage.asset
をTab
の中に入れた場合は何も変化しません。本記事ではその対応方法についてお話します。
標準的な実装 | Image.assetを使用 | 理想型 |
---|---|---|
![]() |
![]() |
![]() |
サンプルコードはここに置いてあります。
https://github.com/shcahill/FlutterTabSample
標準的なTab
の実装
一番素直にTabを実装すると、以下のようになると思います。
TabController _controller;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _controller,
tabs: _buildTabs(context),
),
),
body: TabBarView(
controller: _controller,
children: _buildTabPages(),
),
);
}
/// [Tab]の配列を生成
List<Widget> _buildTabs() {
return [
Tab(icon: Icon(Icons.home), text: 'HOME'),
Tab(icon: Icon(Icons.map), text: 'MAP'),
];
}
/// タブの中身として表示するPage(Widget)の配列を生成
List<Widget> _buildTabPages() {
return [
HomePage(),
MapPage(),
];
}
これを実行すると以下のようになります。
ここでは、選択中のTab
において文字・アイコンが共に白となり、非選択中のアイコンは逆にグレーとなっています。
そしてタブをスクロールさせると、タブインジケータの位置に応じてゆっくりと色が変化していくことがわかります。
Tab
の中身を変えてみる
次はIcons
を使わずにassetに取り込んだpngをImage.asset
で読み込んで表示させます。
List<Widget> _buildTabs() {
return [
Tab(icon: Image.asset('images/soccer.png')),
Tab(icon: Image.asset('images/rugby.png')),
];
}
すると、表示はされるのですが、非選択状態のタブでもアイコンが黒く表示され、グレーにはなりません。
AnimatedBuilder
を使ってタブインジケータの位置に応じてTab
の表示を変化させる
先ほどのTab
をAnimatedBuilderで囲います。
//import 'dart:math' as math;
List<Widget> _buildTabs() {
return [
Tab(child: _buildAnimatedWidget(Image.asset('images/soccer.png'), 0)),
Tab(child: _buildAnimatedWidget(Image.asset('images/rugby.png'), 1)),
];
}
/// [AnimatedBuilder]でラップすることにより、タブインジケータの位置に応じて[child]のopacityを変化させます。
/// [index]は生成する[Tab]のインデックスです
Widget _buildAnimatedWidget(Widget child, int index) {
return AnimatedBuilder(
animation: _controller.animation,
builder: (_, __) {
return Opacity(
child: child,
opacity: _calculateOpacity(index, _controller.animation.value),
);
},
);
}
/// 計算対象の[Tab]の[index]と、タブインジケータのスクロール位置を示す[value]から、[Tab]内のWidgetのOpacityを算出します。
double _calculateOpacity(int index, double value) {
if (index - 1 <= value && value <= index + 1) {
// 選択状態および隣の[Tab]にフォーカスが当たっている状態
final abs = (index - value).abs();
// quiver/iterablesと衝突するため、`math`を付与
return math.max(math.min(1.0 - abs, 1.0), 0.5);
} else {
// 非選択状態
return 0.5;
}
}
すると、TabController
経由でタブインジケータの位置が変わるたびにbuilderが呼ばれ、Opacityの値が変更されるようになります。
以下でAnimatedBuilder
で指定する内容についてみていきます。
AnimatedBuilder#animation
ここには、TabController#animation
を指定します。この値はdouble型(厳密にはAnimation<double>型)で、タブインジケータの位置を示します。例えば、一番左のタブを選択した状態なら0.0、2つ目のタブを選択中なら1.0を示します。つまり仮に3つ目のタブがあれば、この値は0.0~2.0の範囲の値をとることになります。
AnimatedBuilder#builder
ここでは簡単な例としてOpacity
を使ってWidgetの透過度を変更しています。現在のタブの位置はTabController#animation#value
を参照します。その値から透過度を計算しているのが_calculateOpacity
の部分ですが、ここはただの算数ですので説明は割愛します。(このサンプルでは、選択状態ならOpacity=1.0、非選択状態ならOpacity=0.5の値をとるように計算しています)
このようにAnimatedBuilder
の実装次第で好きなようにタブを実装できます。