Flutterのデフォルトのタブ、ださくないですか?
という気持ちに端を発して以下の画像のようなタブを作る方法を解説してみます。
実装
なんとなくSliverで書いた方が難しいかなと思ったのでそちらで書いています。
なお、基本的なTabBarの書き方については過去から特に変わっていないため、解説をしません。
- デフォルトのものを作る場合→ https://flutter.ctrnost.com/basic/navigation/tabbar/
- カスタマイズをする場合→ https://qiita.com/Dreamwalker/items/cc19bb4f8b7068ae0fd3
あたりを参考にしてください。
Size Config
https://medium.com/flutter-community/flutter-effectively-scale-ui-according-to-different-screen-sizes-2cb7c115ea0a
こちらの記事を参考にしています。
要約すると、デバイス間での大きさに誤差を生まないために、サイズは定数で指定するのではなく幅に合わせて指定しようねという話です。
今回は以下のような関数を便利関数として用意しました。
double getProportionHeight(double inputHeight) {
final screenHeight = SizeConfig.screenHeight;
return (inputHeight / 812.0) * screenHeight;
}
double getProportionWidth(double inputWidth) {
final screenWidth = SizeConfig.screenWidth;
return (inputWidth / 375.0) * screenWidth;
}
812x375というのはiPhoneXのサイズです。
デザイナーの方と仕事をする場合、iPhoneXでデザインをしたものをそのまま他のデバイスでも違和感なくみせるためのコードです。
本論とは直接は関係ないのですが、知っておくと便利かと思いますのでご紹介させていただきました。
(参考: https://qiita.com/pe-ta/items/b3b754bde584abc98f44)
タブの実装
とはいっても特に難しいことをするわけではないです。
タイトル部分
以下のような感じで、Expandedを使って左から6割までに配置するようにしました。
// タブの左寄せタイトル郡
Widget _buildTabTitles() {
return Row(
children: [
Expanded(
flex: 3,
child: TabBar(
controller: _tabController,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
width: getProportionWidth(3),
color: Colors.white,
)),
),
labelColor: Colors.white,
unselectedLabelColor: Colors.grey,
tabs: [
_tabTitle('Car'),
_tabTitle('Bike'),
_tabTitle('Walk'),
],
),
),
Expanded(flex: 2, child: Container()),
],
);
}
Widget _tabTitle(String title) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: getProportionWidth(6)),
child: Container(
height: getProportionHeight(40),
width: getProportionWidth(132),
child: Tab(text: title),
),
);
}
アイコン部分
こちらは逆に、Paddingをいれつつ右詰めにしています。
Widget _buildTabActions() {
return Container(
// tab height
height: getProportionWidth(40),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
children: [
_tabAction(
icon: const Icon(Icons.sort_outlined, color: Colors.white),
onTap: () {}),
_tabAction(
icon: const Icon(Icons.filter_alt_outlined, color: Colors.white),
onTap: () {}),
],
),
);
}
Widget _tabAction({@required Icon icon, @required Function onTap}) {
return Padding(
padding: EdgeInsets.only(right: getProportionWidth(20)),
child: GestureDetector(
onTap: () => onTap,
child: icon,
),
);
}
最後にこれらをStackで重ねてあげればWidget自体は完成です。
文字部分は6割まで、アイコン部分は右詰めをするようにしているため、横幅が広い場合でも真ん中にスペースがしっかりとできるようになります。
Widget _buildTabsRow() {
return Stack(
children: [
_buildTabTitles(),
_buildTabActions() // 並び替えとフィルターのactions
],
);
}
PreferredSize Widget
実は上述で作ったWidgetをそのままTabとして使うことはできません。
AppbarのbottomにはPreferredSize
widgetしかいれることができないからです。
なので、最後に PreferredSize
widgetでどれくらいからbottomを開始するのかを指定すれば完成です。
SliverAppBar(
title: Text('Library'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(getProportionHeight(48)),
child: _buildTabsRow())),
参考: https://daichan.club/flutter/78992
コード全文
class LibraryScreen extends StatefulWidget {
const LibraryScreen({Key key}) : super(key: key);
@override
_LibraryScreenState createState() => _LibraryScreenState();
}
class _LibraryScreenState extends State<LibraryScreen>
with SingleTickerProviderStateMixin {
TabController _tabController;
@override
void initState() {
_tabController = TabController(length: 3, vsync: this);
super.initState();
}
@override
void dispose() {
super.dispose();
_tabController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
title: Text('Library'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(getProportionHeight(48)),
child: _buildTabsRow())),
];
},
body: _tabBody()));
}
Widget _tabBody() {
return TabBarView(
controller: _tabController,
children: [
Container(child: Center(child: Icon(Icons.car_rental))),
Container(child: Center(child: Icon(Icons.car_rental))),
Container(child: Center(child: Icon(Icons.car_rental))),
],
);
}
Widget _buildTabsRow() {
return Stack(
children: [
_buildTabTitles(),
_buildTabActions() // 並び替えとフィルターのactions
],
);
}
// タブの左寄せタイトル郡
Widget _buildTabTitles() {
return Row(
children: [
Expanded(
flex: 3,
child: TabBar(
controller: _tabController,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
width: getProportionWidth(3),
color: Colors.black,
)),
),
labelColor: Colors.white,
unselectedLabelColor: Colors.grey,
tabs: [
_tabTitle('Car'),
_tabTitle('Bike'),
_tabTitle('Walk'),
],
),
),
Expanded(flex: 2, child: Container()),
],
);
}
Widget _buildTabActions() {
return Container(
// tab height
height: getProportionWidth(40),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
children: [
_tabAction(
icon: const Icon(Icons.sort_outlined, color: Colors.white),
onTap: () {}),
_tabAction(
icon: const Icon(Icons.filter_alt_outlined, color: Colors.white),
onTap: () {}),
],
),
);
}
Widget _tabAction({@required Icon icon, @required Function onTap}) {
return Padding(
padding: EdgeInsets.only(right: getProportionWidth(20)),
child: GestureDetector(
onTap: () => onTap,
child: icon,
),
);
}
Widget _tabTitle(String title) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: getProportionWidth(6)),
child: Container(
height: getProportionHeight(40),
width: getProportionWidth(132),
child: Tab(text: title),
),
);
}
}