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

  • 2024年6月15日
  • 2024年6月15日
  • Widget

対象者

  • Flutterを使用しているアプリ開発エンジニア
  • TabBarのカスタマイズ方法を詳しく知りたい方
  • アプリのユーザーインターフェースを向上させたいと考えている方

TabBarの基本を知りたい方は、以下を参照してください。

【Flutter】TabBarを使って見やすいレイアウトを設計

はじめに

アプリ開発において、ユーザーインターフェースの質はユーザー体験を大きく左右します。特に、直感的なナビゲーションを提供するためのタブデザインは非常に重要です。Flutterを使ってアプリ開発を進める中で、「もっと魅力的なTabBarを作りたい」と感じたことはありませんか?
この記事では、Flutterを使用してTabBarをカスタマイズするための具体的な方法を詳しく解説します。基本設定からスタイルのカスタマイズ、レイアウトの工夫、タブの個別設定、さらにはインタラクションの微調整まで、多岐にわたるカスタマイズ方法を網羅しています。

FlutterでのTabBarカスタマイズするための第一歩となると思います。あなたのアプリがユーザーにとってより魅力的で使いやすくなるための第一歩として、ぜひお役立てください。

TabBarの配置

TabBarのサンプルをいくつか見ましたが、基本AppBar内に入れているものしか見つかりませんでした。Columnのchild内にTabBarとBatBarViewを配置できるのかな、と思いテストしたら、いけました。

TabBarのスタイルカスタマイズ

色のカスタマイズ

TabBarの色をカスタマイズすることは、アプリのブランドやテーマに合わせてデザインを統一するために重要です。色のカスタマイズは、ユーザーがアプリを使いやすくし、視覚的に魅力的なインターフェースを提供します。

TabBarの色をカスタマイズするためには、labelColorunselectedLabelColor、およびindicatorColorプロパティを使用します。これにより、選択されたタブ、未選択のタブ、インジケーターの色を設定できます。
dividerColorはタブの下線の色を表します。 Colors.transparentを設定し透明にすることで下線を消すことができます。

以下は、色をカスタマイズするためのコード例です。

TabBar(
  labelColor: Colors.blue,
  unselectedLabelColor: Colors.grey,
  indicatorColor: Colors.red,
    dividerColor: Colors.transparent,
)

色のカスタマイズにより、タブの状態が視覚的に明確になり、ユーザーが現在選択されているタブとそうでないタブを一目で識別できるようになります。

フォントとテキストスタイルのカスタマイズ

タブのフォントとテキストスタイルをカスタマイズすることで、アプリのデザインに一貫性を持たせ、読みやすさを向上させることができます。特に、アクセシビリティを向上させるために、フォントサイズやスタイルを調整することが重要です。

TabBarのテキストスタイルをカスタマイズするためには、labelStyleunselectedLabelStyleプロパティを使用します。これにより、選択されたタブと未選択のタブのテキストスタイルを設定できます。

以下は、フォントとテキストスタイルをカスタマイズするためのコード例です。

TabBar(
  labelStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  unselectedLabelStyle: TextStyle(fontSize: 16),
)

フォントとテキストスタイルのカスタマイズにより、タブのテキストが視覚的に強調され、ユーザーが重要な情報を見逃さないようになります。

インジケーターのカスタマイズ

インジケーターのカスタマイズは、選択されたタブを視覚的に強調するための重要な要素です。インジケーターをカスタマイズすることで、アプリのデザインに一貫性を持たせ、ユーザーが現在選択されているタブを容易に識別できるようになります。

インジケーターをカスタマイズするためには、indicatorプロパティを使用します。このプロパティを使って、BoxDecorationを使用してインジケーターの色、形状、ボーダーなどを設定できます。

以下は、インジケーターをカスタマイズするためのコード例です。

TabBar(
  indicator: BoxDecoration(
    color: Colors.green,
    borderRadius: BorderRadius.circular(10),
  ),
  tabs: [
    Tab(icon: Icon(Icons.directions_car), text: 'Car'),
    Tab(icon: Icon(Icons.directions_transit), text: 'Transit'),
    Tab(icon: Icon(Icons.directions_bike), text: 'Bike'),
  ],
)

インジケーターのカスタマイズにより、選択されたタブが視覚的に強調され、ユーザーがアプリを直感的に操作できるようになります。

splashBorderRadiusの設定

splashBorderRadiusを設定することで、タブのリップル効果が発生する領域の形状をカスタマイズできます。これにより、タブのデザインに応じたリップル効果を提供し、デザインの一貫性を保つことができます。リップル効果が発生する領域の角を丸めることで、デザインの統一感を保ち、視覚的に魅力的なインタラクションを提供するためです。

TabBar(
  splashBorderRadius:
      const BorderRadius.vertical(top: Radius.circular(32)),
)

全体として、TabBarのスタイルカスタマイズは、アプリのデザインとユーザーエクスペリエンスを向上させるために不可欠です。色、フォント、インジケーターのカスタマイズを効果的に活用することで、視覚的に魅力的で使いやすいインターフェースを提供できます。

TabBarのレイアウトカスタマイズ

Inkを使ったカスタムレイアウト

TabBarをInkrでラップすることで、TabBarの見た目をカスタマイズできます。高さや背景色、ボーダーの装飾を適用できます。

最初Containerを使用してましたが、その場合タップしたときのエフェクトがおかしかったので、Inkに修正しました。
BorderRadiusを適用することで、タブの選択時や未選択時に視覚的な変化をもたらし、ユーザーの注意を引くことができます。特に、タブのインジケーターや背景に角丸を適用することで、デザインの一貫性を保つことができます。

以下のコード例では、TabBarをInkでラップし、背景色を設定しています。

Ink(
  height: 48,
  decoration: BoxDecoration(
    color: Colors.blue.shade50,
    borderRadius: BorderRadius.circular(8),
  ),
)

このようにすることで、TabBarの見た目を柔軟にカスタマイズでき、アプリ全体のデザインに統一感を持たせることができます。

Tabのカスタマイズ

バッジ付きTabの作成

バッジをタブに追加することで、新しい通知や未読のメッセージなど、ユーザーに重要な情報を視覚的に伝えることができます。バッジは、ユーザーがすぐに注意を向けるべき情報を強調するために使用されます。

以下は、バッジ付きタブアイテムの実例です。

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

TabItemクラスは、タブのアイテムをカスタマイズするためのウィジェットです。このクラスでは、アイコンとテキストを組み合わせてタブを作成し、必要に応じてバッジを表示します。

アイコンとテキストの配置

まず、TabItemクラスはアイコンとテキストを垂直に並べて表示します。これにより、ユーザーがタブの内容を直感的に理解できるようにしています。

バッジの表示

バッジの表示は、count プロパティに基づいて動的に行われます。count が 0 でない場合、バッジが表示されます。このバッジは、新しい通知や未読メッセージの数を示すために使用されます。以下の要素がバッジの表示に関与しています。

  • 位置とマージン: バッジはアイコンとテキストの横に配置され、適切なスペースを確保するためにマージンが設定されています。
  • スタイルと色: バッジの背景色にはテーマの error カラーが使用され、バッジ内のテキストは onError カラーで表示されます。これにより、バッジが目立ち、ユーザーの注意を引くことができます。
  • テキストの内容: バッジに表示されるテキストは、count プロパティの値に基づきます。count が 10未満の場合はそのままの数値が表示され、10 以上の場合は "9+" と表示されます。これにより、多くの通知がある場合でも視覚的にわかりやすくなります。
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 < 9 ? count.toString() : '9+',
        style: TextStyle(color: Theme.of(context).colorScheme.onError),
      ),
    ),
  )

このバッジ表示機能により、ユーザーはタブに関連する重要な通知や未読メッセージを見逃すことなく確認できるようになります。バッジは直感的に理解でき、視覚的に強調されるため、ユーザーエクスペリエンスを向上させる重要な要素となります。

実際は以下のように使用します。

TabBar(
  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),
  ],
),

アイコンとテキストの組み合わせ、バッジ付きタブアイテムの作成、カスタムウィジェットの使用は、タブアイテムのカスタマイズにおいて重要なテクニックです。効果的に活用することで、視覚的に魅力的でユーザーフレンドリーなインターフェースを提供することができます。

Q&A

Q1: TabBarの基本設定方法は?

A1: TabBarの基本設定方法は、Tabウィジェットを使ってタブを作成し、TabControllerでタブの選択状態を管理し、TabBarViewでタブに対応するコンテンツを表示します。これにより、ユーザーは直感的に異なるセクションにアクセスできます。

Q2: TabBarのスタイルをカスタマイズする方法は?

A2: TabBarのスタイルをカスタマイズするには、色やフォント、インジケーターなどのプロパティを設定します。例えば、labelColorやindicatorColorを使って色を変更し、labelStyleやunselectedLabelStyleでテキストスタイルを調整できます。

Q3: TabBarの タッチした時にエフェクトが四角になってしまう

A3: TabBarをカスタマイズする際、TabBarの背景としてContainerやInkを挟むため、それらがタッチエフェクトに影響されます。タッチエフェクトが四角形になる場合は、背景のウィジェットを角丸にする必要があります。これは、Inkで対応してます。
TabBar自体のタッチエフェクトを角丸にするため、、TabBarのsplashBorderRadiusを設定してます。
TabBarに併せてインディケーターも角丸に設定する必要があります。
この3つの設定に、タッチエフェクトが正しく表示されます。

まとめ

この記事を通じて、TabBarの基本設定からスタイル、レイアウト、タブのカスタマイズ、インタラクションなどを勉強しました。これにより、FlutterアプリにおけるTabBarのカスタマイズと応用について深く理解し、視覚的に魅力的で使いやすいインターフェースを作成するスキルを身につけました。

参考

ソース(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);

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Customize TabBar'),
      ),
      body: Column(
        children: [
          const Text('タブの上'),
          Ink(
            height: 48,
            decoration: BoxDecoration(
              borderRadius:
                  const BorderRadius.vertical(top: Radius.circular(32)),
              color: Theme.of(context).colorScheme.primaryContainer,
            ),
            child: TabBar(
              controller: _tabController,
              indicatorSize: TabBarIndicatorSize.tab,
              dividerColor: Colors.transparent,
              indicator: BoxDecoration(
                color: Theme.of(context).colorScheme.primary,
                borderRadius:
                    const BorderRadius.vertical(top: Radius.circular(32)),
              ),
              splashBorderRadius:
                  const BorderRadius.vertical(top: Radius.circular(32)),
              labelColor: Theme.of(context).colorScheme.onPrimary,
              unselectedLabelColor:
                  Theme.of(context).colorScheme.onPrimaryContainer,
              labelStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              unselectedLabelStyle: TextStyle(fontSize: 13),
              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,
              children: const [
                Icon(Icons.directions_car),
                Icon(Icons.directions_transit),
                Icon(Icons.directions_bike),
                Icon(Icons.directions_walk),
              ],
            ),
          ),
          const Text('タブの下'),
        ],
      ),
    );
  }
}

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