FlutterのCupertinoTabBarの高さやTextStyleを自由に変更したい

# はじめに

アプリの画面切り替えに使うTabBarの選択肢として、CupertinoTabBarは便利ですが、
CupertinoTabBarにもう少し設定を加えたい！って思っても、
引数に自分が調整したいプロパティがなくて、困った！ってことはないでしょうか。
そんなときの解決策の案の一つとして、今回の記事を紹介したいと思います。

# 通常のCupertinoTabBarを使用した場合

```dart:main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  // タブに連動して表示させたい画面のリスト
  final List<Widget> _pageWidgets = [
    Center(child: Container(child: Text('ホーム'))),
    Center(child: Container(child: Text('ユーザー'))),
    Center(child: Container(child: Text('お気に入り'))),
    Center(child: Container(child: Text('設定'))),
  ];

  // タブに実際に表示させたいアイコン等のリスト
  final List<BottomNavigationBarItem> _tabItems = [
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      label: 'ホーム',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.person),
      label: 'ユーザー',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.favorite),
      label: 'お気に入り',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.settings),
      label: '設定',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      // 通常のCupertinoTabBarを使用
      tabBar: CupertinoTabBar(
        items: _tabItems,
        activeColor: Colors.yellowAccent,
        inactiveColor: Colors.white,
        backgroundColor: Colors.blueGrey,
      ),
      tabBuilder: (context, index) {
        return CupertinoTabView(builder: (context) {
          return _pageWidgets[index];
        });
      },
    );
  }
}

```

上記のコードを例として、本記事を進めていきます。
こちらの場合は、通常のCupertinoTabBarが使用されており、
見た目に関して、下記のように表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/952081fe-a3a3-c8ea-250c-c2364e854f04.png" width=450 />

通常のCupertinoTabBarの状態では、アイコンと上のタブバーの余白の間隔がかなり狭い印象を受けますね。
CupertinoTabBarは、余白の調整やタブバーの文字に対してTextStyleの設定ができなかったり、
細かく設定したい時に困ります。
なので、どうすべきかというと・・・・


# 解決策

結論、CupertinoTabBarを継承したクラスを作ることです。
例えば、余白を無理矢理作るために、BottomNavigationBarItemに余白分のSizedBoxを追加しても、
サイズエラー等に引っかかりうまくいきません。
また今後別の箇所も変更したい場合を想定すると、クラスを作ったほうがいいと思います。

今回はCupertinoTabBarに高さ(height)と、
タブバーの文字が選択されている状態と通常状態(activeLabelStyleとlabelStyle)を変更できるように、
プロパティを追加した自作クラスを紹介していきたいと思います。

それを実現するようにCupertinoTabBarを継承して作成したCustomCupertinoTabBarは以下の通りになります。

``` dart: custom_cupertino_tab_bar.dart
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

// ナビゲーションバーの基本の高さ
const double _defaultTabBarHeight = 50.0;

const Color _defaultTabBarBorderColor = CupertinoDynamicColor.withBrightness(
  color: Color(0x4C000000),
  darkColor: Color(0x29000000),
);

const Color _defaultTabBarInactiveColor = CupertinoColors.inactiveGray;

class CustomCupertinoTabBar extends CupertinoTabBar {
  CustomCupertinoTabBar({
    Key? key,
    required this.items,
    this.onTap,
    this.currentIndex = 0,
    this.backgroundColor,
    this.activeColor,
    this.inactiveColor = _defaultTabBarInactiveColor,
    this.iconSize = 30.0,
    this.border = const Border(
      top: BorderSide(
        color: _defaultTabBarBorderColor,
        width: 0.0,
        style: BorderStyle.solid,
      ),
    ),
    this.height,
    this.labelStyle,
    this.activeLabelStyle,
  }) : super(items: items);

  final List<BottomNavigationBarItem> items;
  final ValueChanged<int>? onTap;
  final int currentIndex;
  final Color? backgroundColor;
  final Color? activeColor;
  final Color inactiveColor;
  final double iconSize;
  final Border? border;

  // タブバーの高さの設定
  final double? height;
  // タブバーのデフォルトのテキストスタイル
  final TextStyle? labelStyle;
  // タブバーが選択されている状態のテキストスタイル
  final TextStyle? activeLabelStyle;

  @override
  Size get preferredSize =>
      Size.fromHeight(height != null ? height! : _defaultTabBarHeight);

  bool opaque(BuildContext context) {
    final Color backgroundColor =
        this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor;
    return CupertinoDynamicColor.resolve(backgroundColor, context).alpha ==
        0xFF;
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMediaQuery(context));
    final double bottomPadding = MediaQuery.of(context).padding.bottom;

    final Color backgroundColor = CupertinoDynamicColor.resolve(
      this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor,
      context,
    );

    BorderSide resolveBorderSide(BorderSide side) {
      return side == BorderSide.none
          ? side
          : side.copyWith(
              color: CupertinoDynamicColor.resolve(side.color, context));
    }

    final Border? resolvedBorder =
        border == null || border.runtimeType != Border
            ? border
            : Border(
                top: resolveBorderSide(border!.top),
                left: resolveBorderSide(border!.left),
                bottom: resolveBorderSide(border!.bottom),
                right: resolveBorderSide(border!.right),
              );

    final Color inactive =
        CupertinoDynamicColor.resolve(inactiveColor, context);

    final double setHeight = height != null ? height! : _defaultTabBarHeight;

    Widget result = DecoratedBox(
      decoration: BoxDecoration(
        border: resolvedBorder,
        color: backgroundColor,
      ),
      child: SizedBox(
        // タブバーの高さが設定される。
        height: setHeight + bottomPadding,
        child: IconTheme.merge(
          data: IconThemeData(color: inactive, size: iconSize),
          child: DefaultTextStyle(
            style: CupertinoTheme.of(context)
                .textTheme
                .tabLabelTextStyle
                .copyWith(color: inactive),
            child: Padding(
              padding: EdgeInsets.only(bottom: bottomPadding),
              child: Semantics(
                explicitChildNodes: true,
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: _buildTabItems(context),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    if (!opaque(context)) {
      result = ClipRect(
        child: BackdropFilter(
          filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
          child: result,
        ),
      );
    }

    return result;
  }

  List<Widget> _buildTabItems(BuildContext context) {
    final List<Widget> result = <Widget>[];
    final CupertinoLocalizations localizations =
        CupertinoLocalizations.of(context);

    for (int index = 0; index < items.length; index += 1) {
      final bool active = index == currentIndex;
      result.add(
        _wrapActiveItem(
          context,
          Expanded(
            child: Semantics(
              selected: active,
              hint: localizations.tabSemanticsLabel(
                tabIndex: index + 1,
                tabCount: items.length,
              ),
              child: GestureDetector(
                behavior: HitTestBehavior.opaque,
                onTap: onTap == null
                    ? null
                    : () {
                        onTap!(index);
                      },
                child: Padding(
                  padding: const EdgeInsets.only(bottom: 4.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: _buildSingleTabItem(items[index], active),
                  ),
                ),
              ),
            ),
          ),
          active: active,
        ),
      );
    }

    return result;
  }

  List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) {
    return <Widget>[
      Expanded(
        child: Center(child: active ? item.activeIcon : item.icon),
      ),
      if (item.title != null) item.title!,
      if (item.label != null) Text(item.label!),
    ];
  }

  Widget _wrapActiveItem(BuildContext context, Widget item,
      {required bool active}) {
    if (!active) {
      // 設定されたlabelStyleの適用
      if (labelStyle != null) {
        return DefaultTextStyle.merge(
          style: labelStyle,
          child: item,
        );
      } else {
        return item;
      }
    }

    final Color activeColor = CupertinoDynamicColor.resolve(
      this.activeColor ?? CupertinoTheme.of(context).primaryColor,
      context,
    );
    return IconTheme.merge(
      data: IconThemeData(color: activeColor),
      child: DefaultTextStyle.merge(
        // 設定されたactiveLabelStyleが適用される
        style: activeLabelStyle != null
            ? activeLabelStyle
            : TextStyle(color: activeColor),
        child: item,
      ),
    );
  }

  // 今後プロパティを追加する場合、copyWithにも記述する
  // 記述しないとUIに反映されない
  @override
  CustomCupertinoTabBar copyWith({
    Key? key,
    List<BottomNavigationBarItem>? items,
    Color? backgroundColor,
    Color? activeColor,
    Color? inactiveColor,
    double? iconSize,
    Border? border,
    int? currentIndex,
    ValueChanged<int>? onTap,
    double? height,
    TextStyle? labelStyle,
    TextStyle? activeLabelStyle,
  }) {
    return CustomCupertinoTabBar(
      key: key ?? this.key,
      items: items ?? this.items,
      backgroundColor: backgroundColor ?? this.backgroundColor,
      activeColor: activeColor ?? this.activeColor,
      inactiveColor: inactiveColor ?? this.inactiveColor,
      iconSize: iconSize ?? this.iconSize,
      border: border ?? this.border,
      currentIndex: currentIndex ?? this.currentIndex,
      onTap: onTap ?? this.onTap,
      height: height ?? this.height,
      labelStyle: labelStyle ?? this.labelStyle,
      activeLabelStyle: activeLabelStyle ?? this.activeLabelStyle,
    );
  }
}

```

今回のポイントとして、元々のWidgetの表示をあまり変えたくなかったので、
ほぼCupertinoTabBarのソースコードをそのままコピーしています。

また、UIの部分を変化させたいときのポイントについて、
上書きするbuildメソッドに、具体的に表示させるものを記述します。
そしてcopyWithメソッドも記述しないと反映されないので注意。

元々の記述にはなくて、今回のために変更した箇所をフォーカスして説明していきます。

``` dart

// ... 略

class CustomCupertinoTabBar extends CupertinoTabBar {
  CustomCupertinoTabBar({

// ... 略    

    this.height,
    this.labelStyle,
    this.activeLabelStyle,
  }) : super(items: items);

// ... 略

  // タブバーの高さの設定
  final double? height;
  // タブバーのデフォルトのテキストスタイル
  final TextStyle? labelStyle;
  // タブバーが選択されている状態のテキストスタイル
  final TextStyle? activeLabelStyle;

// ... 略

}

```

今回、通常のCupertinoTabBarにはなかった
height, labelStyle, activeLabelStyleプロパティを作成して、
constructorの引数としても受け取れるようにしました。
そして、受け取ったheight等はどう適用されていくかというと・・・


``` dart

// ナビゲーションバーの基本の高さ
const double _defaultTabBarHeight = 50.0;
// ... 略
class CustomCupertinoTabBar extends CupertinoTabBar {
// ... 略    
  final double setHeight = height != null ? height! : _defaultTabBarHeight;
// ... 略  
 @override
  Widget build(BuildContext context) {
// ... 略
  Widget result = DecoratedBox(
// ... 略        
      child: SizedBox(
        // タブバーの高さが設定される。
        height: setHeight + bottomPadding,
// ... 略        
      ),
    );
// ... 略
 return result;
 }
// ... 略
}

```

まず、heightについて、
上記のように、buildメソッドで使われるSizedBoxの高さとして、設定されます。
そして、labelStyleとactiveLabelStyleを見ていくと、

``` dart
// ... 略
class CustomCupertinoTabBar extends CupertinoTabBar {
// ... 略  
 Widget _wrapActiveItem(BuildContext context, Widget item,
      {required bool active}) {
    if (!active) {
      // 設定されたlabelStyleの適用
      if (labelStyle != null) {
        return DefaultTextStyle.merge(
          style: labelStyle,
          child: item,
        );
      } else {
        return item;
      }
    }

    final Color activeColor = CupertinoDynamicColor.resolve(
      this.activeColor ?? CupertinoTheme.of(context).primaryColor,
      context,
    );
    return IconTheme.merge(
      data: IconThemeData(color: activeColor),
      child: DefaultTextStyle.merge(
        // 設定されたactiveLabelStyleが適用される
        style: activeLabelStyle != null
            ? activeLabelStyle
            : TextStyle(color: activeColor),
        child: item,
      ),
    );
  }
// ... 略
}

```

labelStyleとactiveLabelStyleについて、
DefaultTextStyleのmergeメソッド使って、引数のWidgetのテキストスタイルを変更するようにしています。

``` dart
// ... 略
class CustomCupertinoTabBar extends CupertinoTabBar {
// ... 略  
  @override
  CustomCupertinoTabBar copyWith({
// ... 略     
    double? height,
    TextStyle? labelStyle,
    TextStyle? activeLabelStyle,
  }) {
    return CustomCupertinoTabBar(
// ... 略 
      height: height ?? this.height,
      labelStyle: labelStyle ?? this.labelStyle,
      activeLabelStyle: activeLabelStyle ?? this.activeLabelStyle,
    );
  }
}

```

最後にcopyWithメソッドにも記述しないと反映されないので、注意してください。

では、今回作成したクラスを使ってみましょう。
main.dartを変更しましょう。

``` dart: main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// インポートする
import 'custom_cupertino_tabbar.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  // タブに連動して表示させたい画面のリスト
  final List<Widget> _pageWidgets = [
    Center(child: Container(child: Text("ホーム"))),
    Center(child: Container(child: Text("ユーザー"))),
    Center(child: Container(child: Text("お気に入り"))),
    Center(child: Container(child: Text("設定"))),
  ];

  // タブに実際に表示させたいアイコン等のリスト
  final List<BottomNavigationBarItem> _tabItems = [
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      label: 'ホーム',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.person),
      label: 'ユーザー',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.favorite),
      label: 'お気に入り',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.settings),
      label: '設定',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      // 作成したクラスに差し替える
      tabBar: CustomCupertinoTabBar(
        items: _tabItems,
        activeColor: Colors.yellowAccent,
        inactiveColor: Colors.white,
        backgroundColor: Colors.blueGrey,
        // 追加したプロパティを記述する
        height: 80,
        labelStyle: TextStyle(
          color: Colors.white,
          fontWeight: FontWeight.normal,
          fontSize: 12,
        ),
        activeLabelStyle: TextStyle(
          color: Colors.yellowAccent,
          fontWeight: FontWeight.bold,
          fontSize: 14,
        ),
      ),
      tabBuilder: (context, index) {
        return CupertinoTabView(builder: (context) {
          return _pageWidgets[index];
        });
      },
    );
  }
}

```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/8bb48f6f-d6ce-b64e-d682-241464db912e.png" width=450 />

これで、CupertinoTabBarの高さやTextStyleが設定できるようになりました。
他の項目も調整していきたいっていうことであれば、
プロパティの追加とbuildメソッドとcopyWithメソッドに手を加えていく感じになります。

---


# おわり


以上、『CupertinoTabBarの高さやTextStyleを自由に変更したい』についての記事でした。
本記事が誰かの助けになれば嬉しいです。
最後までご覧いただきありがとうございました。


# 参考資料


https://stackoverflow.com/questions/52823841/flutter-overriding-build-method-of-cupertinotabbar