【Flutter】TabBarの表示・非表示を制御

  • 2024年9月6日
  • 2024年9月6日
  • 小物

対象者

  • Flutterで中級レベルのUI操作や状態管理を学びたい方
  • TabBarのカスタマイズや表示切り替えを実装したい方
  • Flutterのアニメーションやタブの管理に関する具体的な実装例を探している方

はじめに

Flutterを使ったアプリ開発では、TabBarによる画面の切り替えはよく利用される機能の一つです。しかし、特定の条件下でTabBarを表示・非表示にする必要がある場合、その制御が少し複雑になることがあります。この記事では、TabBarの表示・非表示を切り替える実装方法と、それに関連する状態管理やスクロール制御について解説します。

TabBarのカスタマイズの詳細は以下をご覧ください。

【Flutter】TabBarカスタマイズでUIをもっと魅力的に

TabBarの表示・非表示切り替えについて

方針

TabBarの表示・非表示を切り替える方法について、Widgetの構造を大幅に変更する必要があると考える方も多いかもしれませんが、実は比較的簡単に実装できます。今回は、Visibilityウィジェットを使ってTabBarの表示を切り替え、加えてタブのスワイプ機能を制御することで、TabBarがない状態と同様の挙動を実現する方法を紹介します。

まず、Visibilityを使用してTabBar自体の表示・非表示を簡単に管理します。通常であれば、TabBarを非表示にした際、TabBarViewのスワイプによるタブの切り替えが可能な状態が続くため、ユーザーに違和感を与えることがあります。この問題を解決するため、TabBarViewのphysicsプロパティを利用してスワイプを無効化します。これにより、TabBarが非表示になった際にスワイプによる切り替えができない状態を作り出し、あたかもタブ自体が存在しないかのような振る舞いを実現できます。

この方法によって、TabBarの表示・非表示を切り替えると同時に、タブのスワイプ機能も無効にでき、UI全体の統一感が保たれます。TabBarView内に不要なWidgetが残ってしまいますが、ソースコードの可読性を重視するため、そのままにしておきます。

以上の手法を使えば、TabBarを簡単に非表示にし、機能も無効化できるため、UIや機能を柔軟にコントロールできます。また、今回の例ではTabBarが非表示の時は2つめの画面(index:1)が表示される前提です。

TabBarの基本構造と仕組み

TabBarは、複数のタブを持つUI要素で、通常、TabBarViewとセットで使用されます。タブを選択することで、それに応じたコンテンツが表示され、ユーザーに複数のページを提供することができます。

TabBar(
  controller: _tabController,
  tabs: const [
    Tab(icon: Icons.directions_car),
    Tab(icon: Icons.directions_transit),
    Tab(icon: Icons.directions_bike),
    Tab(icon: Icons.directions_walk),
  ],
)

このように、TabBarの中にはTabウィジェットを配置し、各タブにアイコンやテキストを設定していきます。

_showTabBarの活用と状態管理

次に、TabBarの表示・非表示を制御するための状態管理について解説します。ここでは、_showTabBarという変数でTabBarの表示フラグを管理し、チェックボックスのオン・オフで切り替えを行います。

var _showTabBar = false;

この変数は、TabBarの表示状態を動的に切り替えるために利用します。CheckboxウィジェットのonChangedメソッドで状態が変わるたびに、TabBarの表示・非表示がトリガーされます。また、タブバーのないときはインデックス1のページが表示されるよう移動してます。

Checkbox(
  value: _showTabBar,
  onChanged: (value) {
    if (value == false) {
      _tabController.animateTo(1);
    }
    setState(() => _showTabBar = value ?? true);
  },
)

このように、チェックボックスの操作に応じてTabBarの表示を切り替えることが可能です。

TabControllerの初期化とvsyncの使い方

TabBarとTabBarViewを連動させるためには、TabControllerが必要です。TabControllerは、タブの数と初期のタブ位置、アニメーション同期を管理します。アニメーションを適切に処理するために、vsyncにはSingleTickerProviderStateMixinを使用します。

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  late final _tabController =
      TabController(length: 4, vsync: this, initialIndex: _showTabBar ? 0 : 1);
  
  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }
}

TabControllervsyncには、このようにthisを渡し、アニメーションのフレーム同期を担当させます。また、タブの数(length)や初期位置(initialIndex)を指定して、タブの状態を管理します。
非表示のときは、initialIndexが1になるようにしてます。

Visibilityウィジェットでのタブバーの表示切り替え

Visibilityウィジェットは、その名前の通り、指定した条件によってウィジェットの表示・非表示を切り替えます。_showTabBarの状態に基づいてTabBarの表示をコントロールします。

Visibility(
  visible: _showTabBar,
  child: TabBar(
    controller: _tabController,
    tabs: const [
      Tab(icon: Icons.directions_car),
      Tab(icon: Icons.directions_transit),
      Tab(icon: Icons.directions_bike),
      Tab(icon: Icons.directions_walk),
    ],
  ),
)

visibleプロパティには、_showTabBarを渡して、チェックボックスの状態に応じてTabBarの表示を切り替えます。Visibilityを使うことで、TabBarが見えない時でもそのスペースがレイアウトに影響を与えないようにできます。

NeverScrollableScrollPhysicsによるスクロール制限

TabBarが非表示の際に、ユーザーが左右にスワイプしてタブを切り替えられないようにするためには、TabBarViewphysicsプロパティを制御します。NeverScrollableScrollPhysicsを利用することで、スクロールを無効化します。

TabBarView(
  controller: _tabController,
  physics: _showTabBar ? null : const NeverScrollableScrollPhysics(),
  children: const [
    Icon(Icons.directions_car),
    Icon(Icons.directions_transit),
    Icon(Icons.directions_bike),
    Icon(Icons.directions_walk),
  ],
)

ここでは、TabBarが表示されている場合は通常のスクロールを許可し、非表示の場合はスクロールを無効にすることで、意図しない画面切り替えを防止しています。

まとめ

今回の記事では、FlutterでのTabBarの表示・非表示の切り替え方法について解説しました。TabControllerを使った状態管理やVisibilityウィジェットを活用することで、柔軟なUI構築が可能です。この実装を応用することで、さらに高度なUIカスタマイズに挑戦できるでしょう。

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  late final _tabController =
      TabController(length: 4, vsync: this, initialIndex: _showTabBar ? 0 : 1);

  var _showTabBar = true;

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Customize TabBar'),
      ),
      body: Column(
        children: [
          SizedBox(
            height: 32,
            child: Checkbox(
              value: _showTabBar,
              onChanged: (value) {
                if (value == false) {
                  _tabController.animateTo(1);
                }
                setState(() => _showTabBar = value ?? true);
              },
            ),
          ),
          Visibility(
            visible: _showTabBar,
            child: TabBar(
              controller: _tabController,
              tabs: const [
                TabItem(icon: Icons.directions_car, text: 'Car', count: 10),
                TabItem(icon: Icons.directions_transit, text: 'Transit'),
                TabItem(icon: Icons.directions_bike, text: 'Bike', count: 9),
                TabItem(icon: Icons.directions_walk, text: 'Walk', count: 0),
              ],
            ),
          ),
          Expanded(
            child: TabBarView(
              controller: _tabController,
              physics:
                  _showTabBar ? null : const NeverScrollableScrollPhysics(),
              children: const [
                Icon(Icons.directions_car),
                Icon(Icons.directions_transit),
                Icon(Icons.directions_bike),
                Icon(Icons.directions_walk),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class TabItem extends StatelessWidget {
  const TabItem({
    super.key,
    required this.icon,
    required this.text,
    this.count = 0,
  });

  final IconData icon;
  final String text;
  final int count;

  @override
  Widget build(BuildContext context) {
    return Tab(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Column(
            children: [
              Icon(icon),
              Text(text),
            ],
          ),
          if (count != 0)
            Container(
              margin: const EdgeInsetsDirectional.only(start: 8),
              padding: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.error,
                shape: BoxShape.circle,
              ),
              child: Center(
                child: Text(
                  count < 10 ? count.toString() : '9+',
                  style:
                      TextStyle(color: Theme.of(context).colorScheme.onError),
                ),
              ),
            )
        ],
      ),
    );
  }
}