【Flutter】SegmentedButtonで効率的な選択肢を作成

対象者

  • Flutterを学び始めたばかりのITエンジニアで、SegmentedButtonの使い方やカスタマイズ方法について詳しくない方
  • モバイルアプリ開発のプロジェクトに参加しており、SegmentedButtonの実装が必要な方
  • 自分のスキルを向上させて、チーム内での信頼を得たいと考えている方

はじめに

SegmentedButtonというのは、日本語で「区分化されたボタン」という意味です。プログラムの世界では一般的に、複数の選択肢から一つまたは複数を選択するためのボタンのグループということを示します。
Flutterにおいては、ユーザーが限られた選択肢から選択するためのウィジェットです。そのため、ユーザーに2つから5つの選択肢を提示し、その中から選択することができます。
実際のアプリとしては、設定画面やフィルタリング機能といったケースに、ユーザーに複数の選択肢から一つまたは複数を選択させるというような機能を実現することができます。

SegmentedButtonの概要

SegmentedButtonは、FlutterのMaterial Designウィジェットの一つで、ユーザーが限られた選択肢から選択できるボタンです。このウィジェットは、2から5の選択肢がある場合に特に有用です。このようなボタンは、ビューの切り替えや要素のソートなど、ユーザーがアプリケーションの特定の部分を制御するためによく使用されます。

SegmentedButtonの作成

SegmentedButtonのコンストラクタ

SegmentedButtonの作成は、そのコンストラクタを通じて行われます。コンストラクタは、少なくとも1つのセグメントを必要としますが、2から5のセグメントが推奨されています。これは、SegmentedButtonが主に少数の選択肢から選択するためのウィジェットであるためです。単一の選択肢が必要な場合は、CheckboxやRadioウィジェットを、5つ以上の選択肢が必要な場合は、FilterChipやChoiceChipウィジェットを検討することが推奨されています。

SegmentedButtonのセグメントの設定

SegmentedButtonの各セグメントは、ButtonSegmentエントリーを使用して設定されます。これらのエントリーは、segmentsフィールド内で定義され、各セグメントを一意に識別するキーと、そのセグメントが表すウィジェットをペアにします。

以下に、SegmentedButtonの作成とセグメントの設定の例を示します。

SegmentedButton<int>(
  onSelectionChanged: (i) {},
  showSelectedIcon: false,
  segments: [
    ButtonSegment(value: 0, label: Text('Option 0')),
    ButtonSegment(value: 1, label: Text('Option 1')),
    ButtonSegment(value: 2, label: Text('Option 2')),
  ],
  selected: {1},
)

この例では、3つのセグメントを持つSegmentedButtonを作成しています。各セグメントは、一意のキー(この場合は0、1、2)と、そのセグメントが表すウィジェット(この場合はテキストウィジェット)をペアにして定義されています。また、初期状態では’Option 1’が選択されています。

SegmentedButtonのカスタマイズ

SegmentedButtonのスタイルプロパティ

SegmentedButtonの見た目は、ButtonStyleプロパティを通じてカスタマイズできます。ButtonStyleプロパティには、ボタン全体に適用されるさまざまなスタイルプロパティが含まれています。これには、ButtonStyle.shadowColor、ButtonStyle.elevation、ButtonStyle.side(外形とセグメント間の区切り線の両方に使用されます)、ButtonStyle.shapeなどがあります。これらのプロパティを適切に設定することで、SegmentedButtonの見た目を自由にカスタマイズできます。

個々のボタンセグメントのスタイル設定

SegmentedButtonの各セグメントは、個別にスタイル設定を行うことができます。これは、各セグメントが独自のウィジェット(例えば、テキストウィジェット)を持つため、そのウィジェットのスタイルプロパティを直接設定することで可能になります。

以下に、SegmentedButtonのスタイルプロパティと各セグメントのスタイル設定の例を示します。

SegmentedButton<int>(
  showSelectedIcon: true,
  style: ButtonStyle(
    backgroundColor: MaterialStateProperty.all(Colors.blue),
    elevation: MaterialStateProperty.all(16),
    shadowColor: MaterialStateProperty.all(Colors.grey),
  ),
  segments: List.generate(3, (index) => index)
      .map((e) => ButtonSegment<int>(
            value: e,
            icon: Icon(Icons.abc),
            label: Text(
              _selected.contains(e) ? '[$e]' : e.toString(),
            ),
          ))
      .toList(),
  selectedIcon: Icon(Icons.plumbing),
),

この例では、SegmentedButton全体の背景色を青に設定し、影の色を灰色に設定し、ボタンの高さを5に設定しています。また、各セグメントのテキスト色を白に設定しています。これにより、SegmentedButton全体と各セグメントが個別にスタイル設定され、見た目がカスタマイズされます。

SegmentedButtonの選択状態の管理

selectedプロパティとは?

SegmentedButtonの選択状態は、selectedプロパティを通じて管理されます。このプロパティは、選択されているセグメントを示すButtonSegment.valueのセットを保持します。SegmentedButton自体は選択状態を保持しないため、onSelectionChangedコールバックが呼び出されるたびにこのプロパティを更新する必要があります。これにより、ユーザーが選択した選択肢を正確に反映することができます。

onSelectionChangedの使用

onSelectionChangedは、ユーザーが新しい選択肢を選択したときに呼び出されるコールバック関数です。この関数は、新しく選択された選択肢を引数として受け取り、その選択肢を反映するために必要な処理を行います。具体的には、この関数内でselectedプロパティを更新することで、SegmentedButtonの選択状態を正確に管理することができます。

以下に、selectedプロパティとonSelectionChangedの使用例を示します。

SegmentedButton<int>(
  onSelectionChanged: (set) {
    setState(() {
      _selected = set;
    });
  },
  .....
  selected: _selected,
),

この例では、ユーザーが新しい選択肢を選択すると、onSelectionChangedが呼び出され、その選択肢がselectedプロパティに反映されます。これにより、SegmentedButtonの選択状態が正確に管理されます。

SegmentedButtonのマルチセレクション

multiSelectionEnabledプロパティの使用

FlutterのSegmentedButtonは、ユーザーが複数のオプションを選択できるようにするマルチセレクションをサポートしています。これは、multiSelectionEnabledプロパティを使用して制御されます。

multiSelectionEnabledプロパティがtrueに設定されている場合、ユーザーはSegmentedButtonの複数のセグメントを選択できます。これは、ユーザーが複数のカテゴリやフィルタを同時に選択できるようにする場合など、アプリケーションによっては非常に便利な機能です。

以下に、multiSelectionEnabledプロパティを使用してマルチセレクションを有効にしたSegmentedButtonのサンプルコードを示します。

SegmentedButton<int>(
  multiSelectionEnabled: true,
)

このコードでは、multiSelectionEnabledがtrueに設定されているため、ユーザーは複数のオプションを選択できます。選択されたオプションは_selectedValuesセットに保存され、選択が変更されるたびに画面が更新されます。

このように、multiSelectionEnabledプロパティを使用することで、SegmentedButtonはさらに強力で柔軟なウィジェットになります。

SegmentedButtonのデザイン

SegmentedButtonの色とアイコンの設定

SegmentedButtonの色とアイコンは、ButtonStyleプロパティとsegmentsフィールドを通じて設定できます。ButtonStyleプロパティは、ボタン全体の背景色や影の色などを設定するために使用されます。一方、segmentsフィールドは、各セグメントのウィジェット(例えば、テキストウィジェットやアイコンウィジェット)を定義するために使用されます。これらのウィジェットは、それぞれ独自の色やアイコンを持つことができます。

また、選択されたセグメントを示すために、選択されたアイコンを表示することもできます。これは、showSelectedIconプロパティをtrueに設定することで可能になります。

SegmentedButtonのレイアウト

SegmentedButtonのレイアウトは、主にButtonStyleプロパティとsegmentsフィールドを通じて制御されます。ButtonStyleプロパティは、ボタン全体の高さや形状などを設定するために使用されます。一方、segmentsフィールドは、各セグメントのウィジェットのレイアウトを制御するために使用されます。

以下に、SegmentedButtonの色とアイコンの設定、およびレイアウトの例を示します。

SegmentedButton<int>(
  onSelectionChanged: (set) {
    setState(() {
      _selected = set;
    });
  },
  showSelectedIcon: false,
  style: ButtonStyle(
    backgroundColor: MaterialStateProperty.all(Colors.blue),
    elevation: MaterialStateProperty.all(16),
    shadowColor: MaterialStateProperty.all(Colors.grey),
  ),
  segments: List.generate(3, (index) => index)
      .map((e) => ButtonSegment<int>(
            value: e,
            icon: Icon(Icons.abc),
            label: Text(
              _selected.contains(e) ? '[$e]' : e.toString(),
            ),
          ))
      .toList(),
  selectedIcon: Icon(Icons.plumbing),
  selected: _selected,
),

この例では、SegmentedButton全体の背景色を青に設定し、影の色を灰色に設定し、ボタンの高さを5に設定しています。また、各セグメントはアイコンとテキストを含むRowウィジェットで構成され、それぞれの色は白に設定されています。これにより、SegmentedButtonの色とアイコンの設定、およびレイアウトがカスタマイズされます。

Q&A

Q1: SegmentedButtonとは何ですか?

A1: SegmentedButtonは、Flutterのウィジェットで、ユーザーが限られた選択肢から選択するためのものです。通常、2から5の選択肢が含まれ、各選択肢はButtonSegmentエントリーを使用して設定されます。

Q2: SegmentedButtonの見た目をカスタマイズするにはどうすればよいですか?

A2: SegmentedButtonの見た目は、ButtonStyleプロパティを通じてカスタマイズできます。このプロパティには、ボタン全体に適用されるさまざまなスタイルプロパティが含まれています。また、各セグメントは、個別にスタイル設定を行うことができます。

Q3: SegmentedButtonの選択状態を管理するにはどうすればよいですか?

A3: SegmentedButtonの選択状態は、selectedプロパティとonSelectionChangedコールバックを通じて管理されます。selectedプロパティは、選択されているセグメントを示すButtonSegment.valueのセットを保持します。onSelectionChangedは、ユーザーが新しい選択肢を選択したときに呼び出されるコールバック関数です。

Q4: SegmentedButtonの無選択状態にするにはどうすればよいですか?

A3: SegmentedButtonのselectedプロパティはSetで管理されていますが、少なくとも一つはデータが入っている必要があります。そのため、無選択にするときになにも設定しないと例外が発生します。そのため、データを設定する必要があります。そこでsegmentsのリストのvalueに含まれていない値を設定すると、無選択状態にすることができます。

まとめ

FlutterのSegmentedButtonは、ユーザーが限られた選択肢から選択するためのウィジェットです。SegmentedButtonの作成は、そのコンストラクタを通じて行われ、2から5のセグメントが推奨されています。各セグメントは、ButtonSegmentエントリーを使用して設定されます。SegmentedButtonの見た目は、ButtonStyleプロパティを通じてカスタマイズできます。また、各セグメントは、個別にスタイル設定を行うことができます。SegmentedButtonの選択状態は、selectedプロパティとonSelectionChangedコールバックを通じて管理されます。最後に、SegmentedButtonの色とアイコンの設定、およびレイアウトは、ButtonStyleプロパティとsegmentsフィールドを通じて制御されます。これらの知識を身につけることで、SegmentedButtonの使い方を理解しました。

重要なポイント:

  • SegmentedButtonは、2から5の選択肢から選択するためのウィジェットです。
    各セグメントは、ButtonSegmentエントリーを使用して設定されます。
  • SegmentedButtonの見た目は、ButtonStyleプロパティを通じてカスタマイズできます。
  • SegmentedButtonの選択状態は、selectedプロパティとonSelectionChangedコールバックを通じて管理されます。
  • SegmentedButtonの色とアイコンの設定、およびレイアウトは、ButtonStyleプロパティとsegmentsフィールドを通じて制御されます。

参考

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

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

enum ColorSelection {
  red(Colors.red),
  green(Colors.green),
  blue(Colors.blue);

  const ColorSelection(this.color);
  final Color color;
}

class _MyHomePageState extends State<MyHomePage> {
  var _selected = <int>{0};

  static const colors = [Colors.red, Colors.green, Colors.blue];
  final _selectedSingleColor = <Color>{colors.first};
  final _selectedMultiColor = <Color>{};
  var _multiColor = Colors.black;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SegmentedButton<int>(
              onSelectionChanged: (set) {
                setState(() {
                  _selected = set;
                });
              },
              showSelectedIcon: true,
              style: ButtonStyle(
                backgroundColor: MaterialStateProperty.resolveWith<Color>(
                  (Set<MaterialState> states) {
                    if (states.contains(MaterialState.selected)) {
                      return Colors.green;
                    }
                    return Colors.red;
                  },
                ),
                elevation: MaterialStateProperty.all(16),
                shadowColor: MaterialStateProperty.all(Colors.grey),
              ),
              segments: List.generate(3, (index) => index)
                  .map((e) => ButtonSegment<int>(
                        value: e,
                        icon: Icon(Icons.abc),
                        label: Text(
                          _selected.contains(e) ? '[$e]' : e.toString(),
                        ),
                      ))
                  .toList(),
              selectedIcon: Icon(Icons.plumbing),
              selected: _selected,
            ),
            const SizedBox(height: 128),
            const Text('single Selection'),
            TweenAnimationBuilder(
              tween: ColorTween(end: _selectedSingleColor.first),
              duration: const Duration(seconds: 1),
              builder: (BuildContext context, Color? color, Widget? child) {
                return Icon(
                  Icons.flutter_dash,
                  size: 64,
                  color: color ?? Colors.red,
                );
              },
            ),
            SegmentedButton<Color>(
              segments: ColorSelection.values
                  .map((e) => ButtonSegment<Color>(
                        value: e.color,
                        label: Text(
                          e.toString(),
                          style: TextStyle(color: e.color),
                        ),
                      ))
                  .toList(),
              selected: _selectedSingleColor,
              multiSelectionEnabled: false,
              onSelectionChanged: (value) => setState(() {
                _selectedSingleColor.clear();
                _selectedSingleColor.addAll(value);
              }),
            ),
            const SizedBox(height: 128),
            const Text('multi Selection'),
            TweenAnimationBuilder(
              tween: ColorTween(end: _multiColor),
              duration: const Duration(seconds: 1),
              builder: (BuildContext context, Color? color, Widget? child) {
                return Icon(
                  Icons.flutter_dash,
                  size: 64,
                  color: color ?? Colors.red,
                );
              },
            ),
            SegmentedButton<Color>(
                segments: ColorSelection.values
                    .map((e) => ButtonSegment<Color>(
                          value: e.color,
                          label: Text(
                            e.toString(),
                            style: TextStyle(color: e.color),
                          ),
                        ))
                    .toList(),
                selected: _selectedMultiColor,
                multiSelectionEnabled: true,
                emptySelectionAllowed: true,
                onSelectionChanged: (value) {
                  _selectedMultiColor.clear();
                  _selectedMultiColor.addAll(value);

                  setState(() => _multiColor = Color.fromARGB(
                        0xFF,
                        _getColor(_selectedMultiColor.contains(Colors.red)),
                        _getColor(_selectedMultiColor.contains(Colors.green)),
                        _getColor(_selectedMultiColor.contains(Colors.blue)),
                      ));
                }),
          ],
        ),
      ),
    );
  }

  int _getColor(bool contains) => contains ? 0xF0 : 0x00;
}