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.
import 'package:flutter/material.dart';
import 'package:flutter_pan_select/flutter_pan_select.dart';
import 'widgets/selection_toolbar.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'flutter_pan_select demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const DemoHome(),
);
}
}
/// Demonstrates two usage styles: the grid uses an implicit controller via
/// `SelectionScope`, while the list uses an explicit `SelectionController`. The
/// top-right AppBar icon toggles the mode; page state is not preserved across
/// toggles (re-entering resets to the initial items).
class DemoHome extends StatefulWidget {
const DemoHome({super.key});
@override
State<DemoHome> createState() => _DemoHomeState();
}
class _DemoHomeState extends State<DemoHome> {
int _currentIndex = 0;
int _nonce = 0;
void _toggleView() {
setState(() => _currentIndex = 1 - _currentIndex);
}
void _refresh() {
setState(() => _nonce++);
}
@override
Widget build(BuildContext context) {
final isGrid = _currentIndex == 0;
return Scaffold(
appBar: AppBar(
title: Text(isGrid ? 'Grid (overlay)' : 'List (left)'),
actions: [
IconButton(
tooltip: 'Refresh',
icon: const Icon(Icons.refresh),
onPressed: _refresh,
),
IconButton(
tooltip: isGrid ? 'Switch to list' : 'Switch to grid',
icon: Icon(
isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
),
onPressed: _toggleView,
),
],
),
body: isGrid
? GridDemo(key: ValueKey('grid-$_nonce'))
: ListDemo(key: ValueKey('list-$_nonce')),
);
}
}
/// Grid + default SelectionItemOverlay + SelectionScope (implicit controller via scope).
class GridDemo extends StatefulWidget {
const GridDemo({super.key});
@override
State<GridDemo> createState() => _GridDemoState();
}
class _GridDemoState extends State<GridDemo> {
final List<int> _items = List.generate(50, (i) => i);
@override
Widget build(BuildContext context) {
return SelectionScope<int>(
child: Stack(
children: [
CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
sliver: SliverPannableGrid<int>.builder(
getItemFromIndex: (index) => _items[index],
itemCount: _items.length,
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
itemBuilder: (context, index) {
return Container(
color: Colors.indigo.shade100,
alignment: Alignment.center,
child: Text('${_items[index]}'),
);
},
),
),
],
),
Positioned(
top: 8,
right: 8,
child: SelectionButton<int>(allItems: _items.toSet()),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: SelectionToolbar<int>(
actions: [
SelectionToolbarAction<int>(
icon: const Icon(Icons.delete_outline),
label: 'Delete',
onPressed: (selected, controller) {
final toRemove = selected.toSet();
setState(() {
_items.removeWhere(toRemove.contains);
});
controller.endSelection();
},
),
],
),
),
],
),
);
}
}
/// List + default SelectionLeftCheckbox + explicit controller (no SelectionScope).
class ListDemo extends StatefulWidget {
const ListDemo({super.key});
@override
State<ListDemo> createState() => _ListDemoState();
}
class _ListDemoState extends State<ListDemo> {
final SelectionController<String> _controller = SelectionController<String>();
final List<String> _items = List.generate(30, (i) => 'Item ${i + 1}');
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.only(bottom: 80),
sliver: SliverPannableList<String>.separated(
controller: _controller,
getItemFromIndex: (index) => _items[index],
itemCount: _items.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
return ListTile(
title: Text(_items[index]),
minTileHeight: 72,
);
},
),
),
],
),
Positioned(
top: 8,
right: 8,
child: SelectionButton<String>(
controller: _controller,
allItems: _items.toSet(),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: SelectionToolbar<String>(
controller: _controller,
actions: [
SelectionToolbarAction<String>(
icon: const Icon(Icons.delete_outline),
label: 'Delete',
onPressed: (selected, controller) {
final toRemove = selected.toSet();
setState(() {
_items.removeWhere(toRemove.contains);
});
controller.endSelection();
},
),
],
),
),
],
);
}
}