今回作成したもの
drag_select_grid_viewパッケージを用いて、以下のようなUIを作成。

下記ソースコードは、本家のサンプルコードを改造したもの。
https://github.com/popy1017/drag_select_grid_view
執筆時点でのバージョン
drag_select_grid_view: ^0.3.1
基本的な使い方
パッケージの説明文にも記載されているが、基本的にはGridView.builderと同じように使うことができるため使いやすい。
return Scaffold(
appBar: SelectionAppBar(
selection: controller.value,
),
body: DragSelectGridView(
gridController: controller,
itemCount: 20,
itemBuilder: (context, index, selected) {
return SelectableItem(
index: index,
selected: selected,
);
},
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 80,
),
),
);
GridView.builderと異なる点
itemBuilder
itemBuilderが若干異なり、BuildContext,intだけでなく、選択状態を表すboolも渡す必要がある。サンプルコードではSelectableItemにbool selectedを渡すことで、選択中の要素とそうでない要素の見た目を変えている。
/// Function signature for creating widgets based on the index and whether
/// it is selected or not.
typedef SelectableWidgetBuilder = Widget Function(
BuildContext context,
int index,
bool selected,
);
DragSelectGridViewController
ScrollControllerの他に、独自のコントローラーDragSelectGridViewControllerを引数に指定できる。pub.devの紹介ページの「Advanced usage」には「DragSelectGridViewController.valueを使うことで選択する要素を直接指定できる」とあり、後述するポイントでも使用する。
その他
その他の追加のパラメータとしては下記の2つ。
-
autoScrollHotspotHeight: オートスクロールが発生する高さを指定できる。この値が大きいと画面の上端・下端から遠くてもオートスクロールが発生するようになる。デフォルトは64。 -
unselectOnWillPop: Routeが閉じるときに選択状態をクリアするかどうか。サンプルではAppBarの左に×ボタンがあり、それを押すとNavigator.maybePopが呼ばれるので選択状態がクリアされる。デフォルトはtrue。
ポイント
「選択モード」のトリガーがロングタップであるため、最初からタップで1つずつ要素を選択することができない。(最初に1つの要素を選択するためには、必ず長押ししないといけない)
Issueでも同様の質問が上がっていたが、このパッケージの解決したい問題とは異なることから対応はされない模様。あくまでドラッグ選択がメインで、タップ選択はおまけ。
暫定対処として、各要素にタップイベントを仕込み、上記で触れたDragSelectGridViewControllerを使って選択した要素のインデックスを追加することにした。SelectableItemにVoidCallback onTapを追加し、example.dartを以下のように修正。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: SelectionAppBar(
selection: controller.value,
title: const Text('Grid Example'),
),
body: DragSelectGridView(
gridController: controller,
padding: const EdgeInsets.all(8),
itemCount: 90,
itemBuilder: (context, index, selected) {
return SelectableItem(
index: index,
color: Colors.blue,
selected: selected,
onTap: () {
final List<int> currentIndexes =
controller.value.selectedIndexes.toList();
if (currentIndexes.contains(index)) {
currentIndexes.remove(index);
} else {
currentIndexes.add(index);
}
controller.value = Selection(currentIndexes.toSet());
},
);
},
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
),
);
controller.value.selectedIndexes.remove()とかで直接編集しようとすると、Unsupported operation: Cannot modify an unmodifiable Setと怒られるので、上記のようにいったんList化しcontoller.valueに代入するような形で更新する。