TL;DR,
-
TabBarをInkで囲む -
Inkの中でcolorを指定する
(2020/3/22修正:MaterialではなくInkを使うように修正)
/// [TabBar]をwrapして背景色を変更するWidget
class ColoredTabBar extends StatelessWidget implements PreferredSizeWidget {
final PreferredSizeWidget tabBar;
final Color color;
// コンストラクタでchildとなる[TabBar]と背景色を指定
ColoredTabBar({@required this.tabBar, @required this.color});
@override
Widget build(BuildContext context) {
// [Ink]でwrapして背景色を指定
return Ink(
color: color,
child: tabBar,
);
}
// [AppBar]のbottomに指定するためには[PreferredSizeWidget]である必要があり、そのためにこのmethodをoverrideします。
// 実態はchildである[TabBar]のpreferredSizeをそのまま使えばOK
@override
Size get preferredSize => tabBar.preferredSize;
}
TabBarで背景色を簡単に変更できない理由
FlutterのTabBarの背景色を変える場合、一手間必要になります。
AppBarと同じ色であれば、TabBarの色も簡単に変更できるのですが、AppBarとは別の色にしたいとなると、少し面倒になります。
やりたいことの割りに少し説明が長くなりますが、順を追って説明します。
TabBarの標準的な使い方
まずはTabBarの一般的な使い方はこちらです。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabController,
tabs: _buildTabs(),
),
),
body: TabBarView(
controller: _tabController,
children: _buildTabPages(),
),
);
}
AppBarとTabBarの背景色は、何が使われているのか
AppBarの色は、AppBarTheme#colorまたはTheme#primaryColorが使用されます。しかし、AppBarにはbackgroundColorというプロパティがあるため、そこで背景色を指定することが可能です。
return Scaffold(
appBar: AppBar(
title: Text('Tab Sample'),
backgroundColor: Colors.blue,
...
一方、TabBarには背景色のパラメータはありません。TabBar自体は透過で、その下にあるAppBarの色が見えています。そのため、基本的には**【TabBarの色を変える=AppBarの色を変える】**ということになります。
たしかに、MaterialDesignのサイトではAppBarの色とTabBarの色が統一されたサンプルしか見たことはないので、基本的にはそれに従うのが良いのかもしれません。(ドキュメントでは明記されてなかったと思いますが。)
ちなみに、TabBarをAppBarの中以外の場所に表示させると、親Widgetの背景色が表示されていることがわかります。
Containerなどで背景色を指定する?
では、TabBarに背景色をつけるにはどうすれば良いでしょうか。
最初に思いつくのは、Containerでラップして色をつけるやり方です。すると、ぱっと見はうまく表示されているように見えます。が、タップしてもRipple Effectが表示されません…
Ripple Effectは背景色の効果のため、今回のようにWidgetに背景色を付けるとRipple Effectが見えなくなってしまうためです。
Ripple Effectについて
少しRipple Effectの説明をします。
Ripple Effectは、InkWellウィジェットでタッチイベントが発生した際に描画されるものですが、このRipple Effectの描画自体はInkWellウィジェットの祖先にあるMaterialウィジェット上で描画されます。
(厳密にはMaterialInkControllerをimplementした_RenderInkFeaturesというRenderObjectで描画されます)
そのため以下のことが言えます。
- 祖先に
Materialが存在しない場合は、描画する人がいないためにRipple Effectは表示されない -
InkWellとMaterilの間のレイヤーに、色を持つWidgetが存在するとRipple Effectが遮られて見えなくなる
// [InkWell]は[InkResponse]を継承したもの。
class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin<T> {
InteractiveInkFeature _createInkFeature(Offset globalPosition) {
// [InkWell]では祖先の[Material]を探索する
final MaterialInkController inkController = Material.of(context);
...
// 祖先の[Material]を使ってRipple Effectを発生させる
InteractiveInkFeature splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create(
controller: inkController,
...
);
return splash;
}
...
}
class Material extends StatefulWidget {
static MaterialInkController of(BuildContext context) {
final _RenderInkFeatures result = context.findAncestorRenderObjectOfType<_RenderInkFeatures>();
return result;
}
...
参考記事)
Flutter 背景色ありで Ripple Effect を効かせる
なぜTabBarでRipple Effectが表示されないのか
以上のことを踏まえた上で、AppBarとTabBarの場合について見ていきます。
まず、AppBarとTabBarの位置関係はこのようになっています。
AppBarとTabBarは縦方向に並んでいるのではなく、AppBarの上にTabBarが重なって乗っています。
ここに、Ripple Effectを表現するために重要となるInkWellとMaterialのレイヤーを表示すると以下のようになります。
AppBarの中にMaterialがあり、TabBarの中にInkWellが存在しています。
コードはこちら。
class _AppBarState extends State<AppBar> {
@override
Widget build(BuildContext context) {
...
return Semantics(
container: true,
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: overlayStyle,
child: Material( // <- ここに[Material]がある
// 背景色は`backgroundColor` > [AppBarTheme] > [Theme]の順に適用されます
color: widget.backgroundColor
?? appBarTheme.color
?? theme.primaryColor,
elevation: widget.elevation
?? appBarTheme.elevation
?? _defaultElevation,
shape: widget.shape,
child: Semantics(
explicitChildNodes: true,
child: appBar,
),
),
),
);
}
...
}
class _TabBarState extends State<TabBar> {
@override
Widget build(BuildContext context) {
...
// Add the tap handler to each tab. If the tab bar is not scrollable,
// then give all of the tabs equal flexibility so that they each occupy
// the same share of the tab bar's overall width.
final int tabCount = widget.tabs.length;
for (int index = 0; index < tabCount; index += 1) {
// ここで[TabBar]の中の[Tab]を[InkWell]でラップしています
wrappedTabs[index] = InkWell(
onTap: () { _handleTap(index); },
child: Padding(
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
child: Stack(
children: <Widget>[
wrappedTabs[index],
Semantics(
selected: index == _currentIndex,
label: localizations.tabLabel(tabIndex: index + 1, tabCount: tabCount),
),
],
),
),
);
...
}
....
}
}
TabBarではTabの配列を持っているため、その各Tabに対してInkWellでラップしています。そして、その各TabのInkWellのRipple Effectを描画するのは祖先であるAppBarの上のMaterialとなります。
先のContainerを使った例では、このAppBar/MaterialとTabBar/InkWellの間に、色付きのContainerを挟んでしまったことにより、AppBar/Material上で描画されているはずのRipple Effectが、色付きContainerに遮られて見えなくなっていました。
Ripple Effectにも対応しつつ、TabBarの背景色を変更する
ここまで分かれば解決策は簡単で、TabBarをInkで囲ってあげれば問題なく動くようになります。InkはMaterial階層化にあるWidgetにRipple Effectを効かせるためのウィジェットで、colorプロパティがあるため、そこで色を指定できます。
また、AppBarのbottomに指定するためにはPreferredSizeWidgetをimplementしている必要があるため、カスタムWidgetを作ります。
class ColoredTabBar extends StatelessWidget implements PreferredSizeWidget {
final PreferredSizeWidget tabBar;
final Color color;
ColoredTabBar({@required this.tabBar, @required this.color});
@override
Widget build(BuildContext context) {
return Ink(
color: color,
child: tabBar,
);
}
@override
Size get preferredSize => tabBar.preferredSize;
}
使うときはTabBarをColoredTabBarで囲ってあげるだけです。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: ColoredTabBar(
color: Colors.red,
tabBar: TabBar(
controller: _controller,
tabs: _buildTabs(),
),
),
),
body: TabBarView(
controller: _tabController,
children: _buildTabPages(),
),
);
}
ちなみにInkウィジェットはInkWellと同様の仕組みを使って、Material上に直接描画しています。(ここは別途まとめたい。)
まとめ
TabBarの背景色を変えるためには
-
TabBarをInkで囲む -
Inkの中でcolorを指定する
補足)
投稿当初、InkではなくMaterialを使った実装を紹介していましたが、@mono0926 さんのご指摘によりInkを使った実装に修正しました
Materialでも動作はしますが、Inkの方がベターです。
InkResponseのドキュメントのTroubleshootingの部分にも記載がありました。
The Ink widget can be used as a replacement for Image, Container, or DecoratedBox to ensure that the image or decoration also paints in the Material itself, below the ink.
また、Ripple Effectまわりの話もTroubleshootingの部分に記載があります。