LoginSignup
14
7

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-24

はじめに

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

通常のCupertinoTabBarを使用した場合

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が使用されており、
見た目に関して、下記のように表示されます。

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

解決策

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

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

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

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メソッドも記述しないと反映されないので注意。

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


// ... 略

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等はどう適用されていくかというと・・・


// ナビゲーションバーの基本の高さ
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を見ていくと、

// ... 略
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のテキストスタイルを変更するようにしています。

// ... 略
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を変更しましょう。

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];
        });
      },
    );
  }
}

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


おわり

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

参考資料

14
7
0

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
14
7