2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Flutter】Segmented buttons を実装する

Posted at

初めに

Material Design3 の Components には Segmented buttons という項目があります。
しかし、Flutter の Widget には Segmented buttons が用意されていないため、今回はそれを実装してみたいと思います。

記事の対象者

  • Flutter と Riverpod の基礎理解ができている方
  • アプリ内で Segmented button を実装する方
  • Material Design3 に記載されていた内容をそのまま実装したい方

準備

今回は material_segmented_control パッケージを使って Segmented button を実装します。

パッケージの追加

まずは material_segmented_control パッケージ を「 pubspeck.yaml 」に記述します。
パッケージのバージョンは、特に制約がなければ最新のバージョンで問題ありません。

pubspeck.yaml
dependencies:
  flutter:
    sdk: flutter

    material_segmented_control: ^4.0.0

Pub get をしてパッケージの準備は完了です。

完成イメージ

以下のように Segmented button によって表示内容が切り替わるようにします。
segmented_button.gif

全体コード

material_segmented_control.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:material_segmented_control/material_segmented_control.dart';

final currentSelectionProvider = StateProvider((ref) => 0);

class MaterialSegmentedControlSample extends ConsumerWidget {
  MaterialSegmentedControlSample({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final int currentSelection = ref.watch(currentSelectionProvider);
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              _customMaterialSegmentedControl(ref),
              const SizedBox(height: 30,),
              currentSelection == 0
                  ? Icon(
                      Icons.flutter_dash,
                      color: Colors.blue.shade300,
                      size: 100,
                    )
                  : currentSelection == 1
                      ? Icon(
                          Icons.flutter_dash,
                          color: Colors.green.shade300,
                          size: 100,
                        )
                      : Icon(
                          Icons.flutter_dash,
                          color: Colors.red.shade300,
                          size: 100,
                        ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _customMaterialSegmentedControl(WidgetRef ref) {
    final StateController<int?> currentSelectionNotifier =
        ref.watch(currentSelectionProvider.notifier);
    final int? currentSelection = ref.watch(currentSelectionProvider);
    return MaterialSegmentedControl(
      children: _children,
      selectionIndex: currentSelection,
      borderColor: Colors.grey,
      selectedColor: currentSelection == 0
          ? Colors.blue.shade100
          : currentSelection == 1
              ? Colors.green.shade100
              : Colors.red.shade100,
      unselectedColor: Colors.white,
      borderRadius: 50.0,
      verticalOffset: 8.0,
      onSegmentChosen: (int index) {
        currentSelectionNotifier.state = index;
      },
    );
  }

  final Map<int, Widget> _children = {
    0: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Blue",
        style: TextStyle(color: Colors.black),
      ),
    ),
    1: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Green",
        style: TextStyle(color: Colors.black),
      ),
    ),
    2: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Red",
        style: TextStyle(color: Colors.black),
      ),
    ),
  };
}

以下の順番でそれぞれ詳しく解説します。

  1. MaterialSegmentedControlSample
  2. _customMaterialSegmentedControl
  3. _children

MaterialSegmentedControlSample

material_segmented_control.dart
final currentSelectionProvider = StateProvider((ref) => 0);

このコードでまず選択されている Segmentedbutton ( 以下ボタン ) の index を監視するための StateProvider を作成しています。
初期値は0に指定しています。

material_segmented_control.dart
final int currentSelection = ref.watch(currentSelectionProvider);

このコードでは先ほど作成した StateProvider を ConsumerWidget 内で使えるように、読み取った値を currentSelection という変数に代入しています。

material_segmented_control.dart
 _customMaterialSegmentedControl(ref),

Column内のこのコードで _customMaterialSegmentedControl() という自分で定義した Widget を表示させています。
この Widget については こちらで解説しています。

material_segmented_control.dart
currentSelection == 0
  ? Icon(
      Icons.flutter_dash,
      color: Colors.blue.shade300,
      size: 100,
    )
  : currentSelection == 1
    ? Icon(
        Icons.flutter_dash,
        color: Colors.green.shade300,
        size: 100,
      )
    : Icon(
        Icons.flutter_dash,
        color: Colors.red.shade300,
        size: 100,
      ),

このコードでは三項演算子をネストにして currentSelection の値に応じて表示させるアイコンの色を変更しています。

_customMaterialSegmentedControl

material_segmented_control.dart
  Widget _customMaterialSegmentedControl(WidgetRef ref) {
    final StateController<int?> currentSelectionNotifier =
        ref.watch(currentSelectionProvider.notifier);
    final int? currentSelection = ref.watch(currentSelectionProvider);
    return MaterialSegmentedControl(
      children: _children,
      selectionIndex: currentSelection,
      borderColor: Colors.grey,
      selectedColor: currentSelection == 0
          ? Colors.blue.shade100
          : currentSelection == 1
              ? Colors.green.shade100
              : Colors.red.shade100,
      unselectedColor: Colors.white,
      borderRadius: 50.0,
      verticalOffset: 8.0,
      onSegmentChosen: (int index) {
        currentSelectionNotifier.state = index;
      },
    );
  }

上のコードが先述した_customMaterialSegmentedControl() の内容です。
以下で内容を詳しくみていきます。

material_segmented_control.dart
  Widget _customMaterialSegmentedControl(WidgetRef ref) {
    final StateController<int?> currentSelectionNotifier =
        ref.watch(currentSelectionProvider.notifier);
    final int? currentSelection = ref.watch(currentSelectionProvider);

このコードではまず Widget 内で Riverpod を使えるように ref を引数として受け取り、現在選択されているボタンを監視するための currentSelectionProvider を読み取っています。

material_segmented_control.dart
    return MaterialSegmentedControl(
      children: _children,
      selectionIndex: currentSelection,
      borderColor: Colors.grey,
      selectedColor: currentSelection == 0
          ? Colors.blue.shade100
          : currentSelection == 1
              ? Colors.green.shade100
              : Colors.red.shade100,
      unselectedColor: Colors.white,
      borderRadius: 50.0,
      verticalOffset: 8.0,
      onSegmentChosen: (int index) {
        currentSelectionNotifier.state = index;
      },
    );

この部分が material_segmented_control パッケージ を使って実装できる部分です。

material_segmented_control.dart
children: _children,

このコードでボタンの子要素を指定しています。
なお、 children として指定できるのは Map 型のみです。
navigation_bar などはアイコンやテキストのリストで指定していたので、Map 型で指定するのは珍しいと言えるのではないでしょうか。
_children はこちらで解説しています。

material_segmented_control.dart
selectionIndex: currentSelection,

このコードで、現在選択されているボタンのインデックスを切り替えています。
このように指定することで、 currentSelection の値が切り替わった際に選択されているボタンが切り替わります。

material_segmented_control.dart
borderColor: Colors.grey,

このコードではボタンの枠線の色を指定しています。
何も指定しなければグレーの枠線になります。

material_segmented_control.dart
selectedColor: currentSelection == 0
  ? Colors.blue.shade100
  : currentSelection == 1
    ? Colors.green.shade100
    : Colors.red.shade100,

このコードではボタンが選択されている時の色を currentSelection の値をもとに三項演算子で変更しています。
「完成イメージ」ではボタンが選択された状態でもテキストは黒色のままでしたが、本体は selectedColor を指定するとボタンの背景色とテキストの両方の色が変更されます。

何も指定しなかった場合は以下のように青色になります。
スクリーンショット 2022-09-10 14.48.22.png

material_segmented_control.dart
unselectedColor: Colors.white,

このコードではプロパティの名前通り、選択されていないボタンの色を指定します。
コードでは白色にしていますが、何も指定しなかった場合は以下のように黒っぽい色になります。
スクリーンショット 2022-09-10 14.50.57.png

material_segmented_control.dart
borderRadius: 50.0,

borderRadius ではボタンの角の丸みを調整しています。

borderRadius: 0,
このように指定すると以下のように全く丸みがなくなります。
スクリーンショット 2022-09-10 14.54.06.png

material_segmented_control.dart
verticalOffset: 8.0,

このコードではボタンの子要素の垂直方向の Padding にあたる空白を調整しています。

verticalOffset: 30.0,
このように値を大きくしてみると、以下のように子要素であるテキストとボタンの枠線の間が垂直方向に大きくなることがわかります。
スクリーンショット 2022-09-10 14.58.08.png

material_segmented_control.dart
onSegmentChosen: (int index) {
  currentSelectionNotifier.state = index;
},

このコードではボタンが選択された時の処理を記述しています。
引数として index を受け取り、currentSelectionNotifier.state に代入しています。

currentSelectionNotifier.state が変更されることにより、選択されているボタンのインデックスが変更され、ボタンが切り替わります。

_children

material_segmented_control.dart
  final Map<int, Widget> _children = {
    0: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Blue",
        style: TextStyle(color: Colors.black),
      ),
    ),
    1: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Green",
        style: TextStyle(color: Colors.black),
      ),
    ),
    2: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 8.0),
      child: Text(
        "Red",
        style: TextStyle(color: Colors.black),
      ),
    ),
  };

このコードではボタンの子要素にあたる Widget を記述しています。
MaterialSegmentedControl() の子要素は全て Map型で指定する必要があるため、ボタンのインデックスと表示内容がセットになった Map型が _children に代入されています。

final Map<int, Widget> _children = {
  ボタンのインデックス : ボタンで表示させる Widget 
}

以上です!

あとがき

最後まで読んでいただきありがとうございました。

今回はボタンに応じて表示内容を変更させるために三項演算子を使用しました。
よく考えてみれば当たり前のことなのですが、if文における else if が三項演算子でも実装できると分かり、まだまだ知らないことだらけなのだと実感しました。

参考にしていただければ幸いです。
誤っている箇所があればご指摘いただければ幸いです。

参考にしたサイト

2
0
0

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
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?