Flutter-SegmentedButton

ToggleButtonsを使っている実装をどのようにすればSegmentedButtonに変更できるか検討してみたい。

きっかけ

Flutter-InkWellでToggleButtonsを調べていて、代替とSegmentedButtonがあるよということに気が付いた。

もうちょっと資料をよく見たら以下のような記述があった。

ToggleButtons, a similar widget that was built for Material 2.
SegmentedButton should be considered as a replacement for ToggleButtons.

ToggleButtons は、Material 2 用に作成された同様のウィジェットです。
SegmentedButton は、ToggleButton の代わりと見なす必要があります。

後、Material 2についても調べてみたらそこには以下のような記述があった。

Support for Material 2 will eventually be removed according to Flutter’s deprecation policy.

マテリアル 2 のサポートは、Flutter の非推奨ポリシーに従って最終的に削除されます。

つまり、ToggleButtonsは将来非推奨になるかもしれないのでSegmentedButtonを使いましょうということらしい。

呼び出し方の比較

  • ToggleButtons
    子供のWidgetとその選択状態をListで渡す形になっている。
    タップ(マウス押下)されたWidgetは、その位置を番号としてonPressedに呼び出しがされる。
  • SegmentedButton<T>
    子供のWidgetはList<ButtonSegment<T>>で渡し、選択状態をSet<T>で渡す。
    タップ(マウス押下)されたButtonSegment<T>はonSelectionChangedで現在の選択状態をSet<T>に入れ呼び出される。

上を見てわかる通り、呼び出しの構造自体が異なる。

変換用ウィジェット

ということでToggleButtonsの呼び出し方でSegmentedButton<T>を生成するウィジェットを作ってみる。

肝となるのは、以下の点。

  1. List<bool>の選択情報をSet<int>に変換する。
    この変換ウィジェットではSegmentedButton<int>として生成するようにするので、選択状態はSet<int>で与えるようにしている。
  2. List<Widget>のchildren情報をButtonSegment<int>に変換する。
    ButtonSegmentで設定可能なのはiconとlabelとパラメータに書かれているのだけど、中身はTextButtonで構築されていて、labelにアイコンを設定しても特に問題はなさそうだった。
  3. onSelectionChangedでToggleButtons.onPressedの呼び出し様式に変換する。

一応上記の様な仕様で考えた結果としては、以下の様な感じで実現できた。

上がオリジナルなToggleButtons、下が変換ウィジェット経由で実現したSegmentedButton。

ボタンの形状が異なるのは、ちょっと微調整必要だけど。

以下がその変換用ウィジェット。ToggleButtonsの名称をToggleToSegmentに変えるだけで使える。

/// ToggleButtonsのAPIそのままででSegmentedButtonにするクラス
/// 形状の合わせこみまではしていない
class ToggleToSegment extends StatelessWidget {
  /// The corresponding selection state of each toggle button.
  /// Each value in this list represents the selection state of the [children] widget at the same index.
  /// The length of [isSelected] has to match the length of [children].
  final List<bool> isSelected;

  /// The callback that is called when a button is tapped.
  /// The index parameter of the callback is the index of the button that is tapped or otherwise activated.
  /// When the callback is null, all toggle buttons will be disabled.
  final void Function(int)? onPressed;

  /// The toggle button widgets.
  /// These are typically [Icon] or [Text] widgets. The boolean selection state of each widget is defined by the corresponding [isSelected] list item.
  /// The length of children has to match the length of [isSelected]. If [focusNodes] is not null, the length of children has to also match the length of [focusNodes].
  final List<Widget> children;

  const ToggleToSegment(
      {required this.isSelected,
      required this.children,
      this.onPressed,
      super.key})
      : assert(isSelected.length == children.length);

  /// [isSelected]のList<bool>をSegmentedButtonに渡すSet<int>形式に
  /// 変換
  Iterable<int> _selected() sync* {
    for (int i = 0; i < isSelected.length; i++) {
      if (isSelected[i]) {
        yield i;
      }
    }
  }

  /// [children]のウィジェットをButtonSegment形式に変換
  Iterable<ButtonSegment<int>> _buttons() sync* {
    for (int i = 0; i < children.length; i++) {
      yield ButtonSegment<int>(value: i, label: children[i]);
    }
  }

  @override
  Widget build(BuildContext context) {
    return SegmentedButton<int>(
      segments: [..._buttons()],
      selected: <int>{..._selected()},
      onSelectionChanged: (Set<int> newSelection) {
        if (onPressed != null) {
          for (int i = 0; i < isSelected.length; i++) {
            if ((isSelected[i] && !newSelection.contains(i)) ||
                (!isSelected[i] && newSelection.contains(i))) {
              onPressed!(i);
            }
          }
        }
      },
      multiSelectionEnabled: true,
      emptySelectionAllowed: true,
      showSelectedIcon: false,
    );
  }
}

選択状態とウィジェットのパラメータの変換は、別途関数を用意しasync*/yieldで構成しIterableを返すようにし、build内ではその関数経由でSet/Listを構築してSegmentedButtonを作るようにしている。

コメント

タイトルとURLをコピーしました