LoginSignup
29
17

More than 3 years have passed since last update.

[Flutter]TabBarの背景色をAppBarと異なる色に変更する

Last updated at Posted at 2020-03-21

TL;DR,

  • TabBarInkで囲む
  • Inkの中でcolorを指定する

(2020/3/22修正:MaterialではなくInkを使うように修正)

TabBarの背景色を変えるカスタムウィジェット
/// [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の一般的な使い方はこちらです。

TabBarの一般的な使い方
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      bottom: TabBar(
        controller: _tabController,
        tabs: _buildTabs(),
      ),
    ),
    body: TabBarView(
      controller: _tabController,
      children: _buildTabPages(),
    ),
  );
}

標準的なTabBarの使い方

AppBarTabBarの背景色は、何が使われているのか

AppBarの色は、AppBarTheme#colorまたはTheme#primaryColorが使用されます。しかし、AppBarにはbackgroundColorというプロパティがあるため、そこで背景色を指定することが可能です。

AppBarの背景色指定
return Scaffold(
  appBar: AppBar(
    title: Text('Tab Sample'),
    backgroundColor: Colors.blue,
    ...

一方、TabBarには背景色のパラメータはありません。TabBar自体は透過で、その下にあるAppBarの色が見えています。そのため、基本的にはTabBarの色を変える=AppBarの色を変える】ということになります。

たしかに、MaterialDesignのサイトではAppBarの色とTabBarの色が統一されたサンプルしか見たことはないので、基本的にはそれに従うのが良いのかもしれません。(ドキュメントでは明記されてなかったと思いますが。)

ちなみに、TabBarAppBarの中以外の場所に表示させると、親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は表示されない
  • InkWellMaterilの間のレイヤーに、色を持つWidgetが存在するとRipple Effectが遮られて見えなくなる
ink_well.dart
// [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;
  }
  ...
}
material.dart
class Material extends StatefulWidget {
 static MaterialInkController of(BuildContext context) {
    final _RenderInkFeatures result = context.findAncestorRenderObjectOfType<_RenderInkFeatures>();
    return result;
  }
  ...

参考記事)
Flutter 背景色ありで Ripple Effect を効かせる

なぜTabBarでRipple Effectが表示されないのか

以上のことを踏まえた上で、AppBarTabBarの場合について見ていきます。
まず、AppBarTabBarの位置関係はこのようになっています。

AppBarとTabBar

AppBarTabBarは縦方向に並んでいるのではなく、AppBarの上にTabBarが重なって乗っています。
ここに、Ripple Effectを表現するために重要となるInkWellMaterialのレイヤーを表示すると以下のようになります。

InkWellとMaterial

AppBarの中にMaterialがあり、TabBarの中にInkWellが存在しています。
コードはこちら。

app_bar.dart
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,
          ),
        ),
      ),
    );
  }
  ...
}
tabs.dart
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でラップしています。そして、その各TabInkWellのRipple Effectを描画するのは祖先であるAppBarの上のMaterialとなります。

先のContainerを使った例では、このAppBar/MaterialTabBar/InkWellの間に、色付きのContainerを挟んでしまったことにより、AppBar/Material上で描画されているはずのRipple Effectが、色付きContainerに遮られて見えなくなっていました。

Ripple Effectにも対応しつつ、TabBarの背景色を変更する

ここまで分かれば解決策は簡単で、TabBarInkで囲ってあげれば問題なく動くようになります。InkMaterial階層化にあるWidgetにRipple Effectを効かせるためのウィジェットで、colorプロパティがあるため、そこで色を指定できます。
また、AppBarbottomに指定するためにはPreferredSizeWidgetをimplementしている必要があるため、カスタムWidgetを作ります。

TabBarの背景色を変えるカスタムウィジェット
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;
}

使うときはTabBarColoredTabBarで囲ってあげるだけです。

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の背景色を変えるためには

  • TabBarInkで囲む
  • Inkの中でcolorを指定する

補足)

投稿当初、InkではなくMaterialを使った実装を紹介していましたが、@mono0926 さんのご指摘によりInkを使った実装に修正しました:pray: 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の部分に記載があります。

29
17
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
17