LoginSignup
16

More than 1 year has passed since last update.

Organization

FlutterでちょっとイケてるTabを書く

Flutterのデフォルトのタブ、ださくないですか?

という気持ちに端を発して以下の画像のようなタブを作る方法を解説してみます。

スクリーンショット 2020-12-11 0.22.20.png

実装

なんとなくSliverで書いた方が難しいかなと思ったのでそちらで書いています。
なお、基本的なTabBarの書き方については過去から特に変わっていないため、解説をしません。

あたりを参考にしてください。

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

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
What you can do with signing up
16