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の部分に記載があります。