flutter_pan_select 1.0.0
flutter_pan_select: ^1.0.0 copied to clipboard
Multi-select with drag-to-select (pan) gesture for grids and lists. Lightweight, themeable selection controller with marquee selection and auto-scroll support.
flutter_pan_select #
English | 繁體中文
Multi-select with drag-to-select (pan) gesture for grids and lists. Lightweight, themeable selection controller with marquee selection and auto-scroll support.
Features #
- Drag to select — pan across items to select a range, the file-manager / photo-gallery pattern (not a dropdown or dialog picker).
- Grid and list — works over both
SliverGridandSliverList. - Edge auto-scroll — dragging toward the top or bottom edge scrolls the view and keeps selecting the items revealed under the finger.
- Default or custom checkboxes — a built-in checkbox per layout, or supply
your own decoration via
checkboxBuilder. - Bring your own state — explicit
SelectionController(Riverpod / Bloc / GetX friendly) or an implicitSelectionScope. - Object-based selection — selected items come back as a
Set<T>of your own values, stable across reordering.
Installation #
dependencies:
flutter_pan_select: ^1.0.0
Quick start #
The easy way: SelectionScope #
Wrap the subtree with SelectionScope<T> and the widgets pick up the
controller automatically:
SelectionScope<Photo>(
child: CustomScrollView(
slivers: [
SliverPannableGrid<Photo>.builder(
getItemFromIndex: (i) => photos[i],
itemCount: photos.length,
crossAxisCount: 3,
// Each item is wrapped with SelectionItemOverlay automatically.
itemBuilder: (context, index) => PhotoTile(photo: photos[index]),
),
],
),
);
To enter selection mode, grab the controller with
SelectionScope.of<Photo>(context).startSelection(allItems: photos.toSet()).
The explicit way: pass your own controller #
If you already manage selection state with Riverpod / Bloc / GetX, hold a
SelectionController<T> in your store and pass it in:
SliverPannableList<String>.separated(
controller: myController,
getItemFromIndex: (i) => items[i],
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(),
// Each item is wrapped with SelectionLeftCheckbox automatically.
itemBuilder: (context, index) => ListTile(title: Text(items[index])),
);
This is the same pattern Flutter itself uses for TabController /
DefaultTabController. No SelectionScope ancestor needed.
Customising or disabling the checkbox #
SliverPannableList / SliverPannableGrid wrap each item with their default
checkbox (SelectionLeftCheckbox / SelectionItemOverlay). Override it with
checkboxBuilder, or opt out by returning the child unchanged:
// Custom selection decoration
SliverPannableGrid<Photo>.builder(
// ...
checkboxBuilder: (context, item, child) =>
MyBadge(item: item, child: child),
);
// No automatic wrapping — decorate it yourself in itemBuilder
SliverPannableList<String>.separated(
// ...
checkboxBuilder: (context, item, child) => child,
);
Public API #
| Type | Purpose |
|---|---|
SelectionController<T> |
Holds selection state. Extends ChangeNotifier. |
SelectionScope<T> |
InheritedWidget hosting a controller. Optional. |
SelectionButton<T> |
Toggles selection mode on/off (startSelection / endSelection). |
SelectionLeftCheckbox<T> |
List-style checkbox on the left of each item. Default wrapper for SliverPannableList. |
SelectionItemOverlay<T> |
Grid-style checkbox overlay at the top-right. Default wrapper for SliverPannableGrid. |
SliverPannable<T> |
Core sliver that detects pan-to-select gestures. Layout-agnostic; no default wrapper. |
SliverPannableGrid<T>.builder |
Wrapper over SliverPannable + SliverGrid. Wraps items with SelectionItemOverlay by default; override via checkboxBuilder. |
SliverPannableList<T>.separated |
Wrapper over SliverPannable + SliverList.separated. Wraps items with SelectionLeftCheckbox by default; override via checkboxBuilder. |
How it works #
Pan select hit-tests against on-screen items using GlobalKeys. Only items
currently inside the viewport have a RenderBox and are reachable by the
gesture; off-screen items are skipped. Edge auto-scroll brings new items
into the viewport as the user drags toward the top or bottom edge.
SliverPannable requires a Scrollable ancestor (it grabs the
ScrollController from Scrollable.of(context)), and only supports
vertical scrolling in this release.
FAQ #
Q: Can I use this with Riverpod / Bloc / GetX?
Yes. Hold a SelectionController<T> wherever you keep your other state and
pass it to the widgets via the controller: parameter. SelectionScope is
optional.
Q: I have two selection lists on one page with the same T. How?
Use two separate controllers and pass them explicitly to each subtree's
widgets. The implicit SelectionScope.of<T>(context) lookup resolves to the
nearest ancestor SelectionScope<T>; with nested or sibling scopes sharing
the same T, explicit injection ensures each subtree connects to the intended
controller.
Q: How do I customise the bottom toolbar?
The package intentionally does not ship a toolbar widget. See
example/lib/widgets/selection_toolbar.dart for a reference implementation
you can copy and modify, or build your own around
SelectionScope.of<T>(context) / your controller.
Q: Why does T need to be value-equal?
The controller stores selected items in a Set<T>, so T must have stable
== and hashCode. Plain value-classes, records, and enum types all work.
Known limitations #
- Horizontal scrolling is not supported (asserted in
SliverPannable). - Pan select can only act on items currently inside the viewport.
SelectionScope.of<T>(context)resolves to the nearest matching ancestor; if you have nested scopes sharing the sameT, use explicitcontroller:injection to disambiguate.
License #
MIT