はじめに
アプリの画面切り替えに使うTabBarの選択肢として、CupertinoTabBarは便利ですが、
CupertinoTabBarにもう少し設定を加えたい!って思っても、
引数に自分が調整したいプロパティがなくて、困った!ってことはないでしょうか。
そんなときの解決策の案の一つとして、今回の記事を紹介したいと思います。
通常のCupertinoTabBarを使用した場合
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は以下の通りになります。
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を変更しましょう。
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を自由に変更したい』についての記事でした。
本記事が誰かの助けになれば嬉しいです。
最後までご覧いただきありがとうございました。
参考資料