Skip to content

Instantly share code, notes, and snippets.

@mkiisoft
Forked from slightfoot/multi_select.dart
Created September 2, 2019 18:11
Show Gist options
  • Save mkiisoft/1a07ddbff426ad7bcf47f74323e409f4 to your computer and use it in GitHub Desktop.
Save mkiisoft/1a07ddbff426ad7bcf47f74323e409f4 to your computer and use it in GitHub Desktop.
Multi Select GridView in Flutter - by Simon Lightfoot (Drag to select multiple items)
// MIT License
//
// Copyright (c) 2019 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.indigo,
accentColor: Colors.pinkAccent,
),
home: ExampleScreen(),
),
);
}
class ExampleScreen extends StatefulWidget {
@override
_ExampleScreenState createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
final colors = <MaterialColor>[];
var _selection = MultiChildSelection.empty;
@override
void initState() {
super.initState();
colors.addAll(Colors.primaries);
colors.addAll(Colors.primaries);
colors.addAll(Colors.primaries);
colors.addAll(Colors.primaries);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MultiSelectAppBar(
appBar: AppBar(
title: Text('Multi Select Example'),
),
selection: _selection,
selectingActions: <Widget>[
IconButton(
icon: Icon(Icons.share),
onPressed: () {},
)
],
),
body: MultiSelectGridView(
onSelectionChanged: (selection) => setState(() => _selection = selection),
padding: EdgeInsets.all(8.0),
itemCount: colors.length,
itemBuilder: (BuildContext context, int index, bool selected) {
return ExampleChildItem(
index: index,
color: colors[index],
selected: selected,
);
},
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
),
),
);
}
}
class ExampleChildItem extends StatefulWidget {
const ExampleChildItem({
Key key,
@required this.index,
@required this.color,
@required this.selected,
}) : super(key: key);
final int index;
final MaterialColor color;
final bool selected;
@override
_ExampleChildItemState createState() => _ExampleChildItemState();
}
class _ExampleChildItemState extends State<ExampleChildItem> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: kThemeChangeDuration, vsync: this);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.8).animate(CurvedAnimation(
parent: _controller,
curve: Curves.ease,
));
}
@override
void didUpdateWidget(ExampleChildItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selected) {
_controller.forward();
} else {
_controller.reverse();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (BuildContext context, Widget child) {
final color = Color.lerp(widget.color.shade500, widget.color.shade900, _controller.value);
return Transform.scale(
scale: _scaleAnimation.value,
child: DecoratedBox(
decoration: BoxDecoration(color: color),
child: child,
),
);
},
child: Container(
alignment: Alignment.center,
child: Text(
'${widget.index}',
style: TextStyle(
color: Colors.white,
fontSize: 22.0,
),
),
),
);
}
}
class MultiSelectAppBar extends StatelessWidget implements PreferredSizeWidget {
const MultiSelectAppBar({
Key key,
this.appBar,
this.selection = MultiChildSelection.empty,
this.selectingActions,
}) : assert(selection != null),
super(key: key);
final AppBar appBar;
final MultiChildSelection selection;
final List<Widget> selectingActions;
@override
Widget build(BuildContext context) {
return Container(
color: appBar.backgroundColor ?? AppBarTheme.of(context).color ?? Theme.of(context).primaryColor,
child: AnimatedSwitcher(
duration: kThemeAnimationDuration,
child: !selection.selecting
? appBar
: AppBar(
key: Key('multi-select'),
leading: const BackButton(),
titleSpacing: 0.0,
title: Text('${selection.total} items selected…'),
actions: selectingActions,
),
),
);
}
@override
Size get preferredSize => appBar.preferredSize;
}
typedef MultiSelectWidgetBuilder = Widget Function(BuildContext context, int index, bool selected);
typedef MultiSelectSelectionCb = void Function(MultiChildSelection selection);
class MultiSelectGridView extends StatefulWidget {
const MultiSelectGridView({
Key key,
this.onSelectionChanged,
this.padding,
@required this.itemCount,
@required this.itemBuilder,
@required this.gridDelegate,
this.scrollPadding = const EdgeInsets.all(48.0),
}) : super(key: key);
final MultiSelectSelectionCb onSelectionChanged;
final EdgeInsetsGeometry padding;
final int itemCount;
final MultiSelectWidgetBuilder itemBuilder;
final SliverGridDelegate gridDelegate;
final EdgeInsets scrollPadding;
@override
_MultiSelectGridViewState createState() => _MultiSelectGridViewState();
}
class _MultiSelectGridViewState extends State<MultiSelectGridView> {
bool _isSelecting = false;
int _startIndex = -1;
int _endIndex = -1;
LocalHistoryEntry _historyEntry;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapUp: _onTapUp,
onLongPressStart: _onLongPressStart,
onLongPressMoveUpdate: _onLongPressUpdate,
onLongPressEnd: _onLongPressEnd,
child: IgnorePointer(
ignoring: _isSelecting,
child: GridView.builder(
padding: widget.padding,
itemCount: widget.itemCount,
itemBuilder: (BuildContext context, int index) {
final start = (_startIndex < _endIndex) ? _startIndex : _endIndex;
final end = (_startIndex < _endIndex) ? _endIndex : _startIndex;
final selected = (index >= start && index <= end);
return _MultiSelectChild(
index: index,
child: widget.itemBuilder(context, index, selected),
);
},
gridDelegate: widget.gridDelegate,
),
),
);
}
void _onTapUp(TapUpDetails details) {
_setSelection(-1, -1);
_updateLocalHistory();
}
void _onLongPressStart(LongPressStartDetails details) {
final startIndex = _findMultiSelectChildFromOffset(details.localPosition);
_setSelection(startIndex, startIndex);
setState(() => _isSelecting = (startIndex != -1));
}
void _onLongPressUpdate(LongPressMoveUpdateDetails details) {
if (_isSelecting) {
_updateEndIndex(details.localPosition);
}
}
void _onLongPressEnd(LongPressEndDetails details) {
_updateEndIndex(details.localPosition);
setState(() => _isSelecting = false);
}
void _updateEndIndex(Offset localPosition) {
final endIndex = _findMultiSelectChildFromOffset(localPosition);
if (endIndex != -1) {
_setSelection(_startIndex, endIndex);
_updateLocalHistory();
}
}
void _setSelection(int start, int end) {
print('selection $start, $end');
setState(() {
_startIndex = start;
_endIndex = end;
});
if (widget.onSelectionChanged != null) {
final start = (_startIndex < _endIndex) ? _startIndex : _endIndex;
final end = (_startIndex < _endIndex) ? _endIndex : _startIndex;
final total = (start != -1 && end != -1) ? ((end + 1) - start) : 0;
widget.onSelectionChanged?.call(MultiChildSelection(total, start, end));
}
}
void _updateLocalHistory() {
final route = ModalRoute.of(context);
if (route != null) {
if (_startIndex != -1 && _endIndex != -1) {
if (_historyEntry == null) {
_historyEntry = LocalHistoryEntry(
onRemove: () {
_setSelection(-1, -1);
_historyEntry = null;
},
);
route.addLocalHistoryEntry(_historyEntry);
}
} else {
if (_historyEntry != null) {
route.removeLocalHistoryEntry(_historyEntry);
_historyEntry = null;
}
}
}
}
int _findMultiSelectChildFromOffset(Offset offset) {
final ancestor = context.findRenderObject();
int result = -1;
void visitChildren(Element element) {
final RenderObject box = element.renderObject;
if (box is _RenderMultiSelectChild) {
final rect = box.localToGlobal(Offset.zero, ancestor: ancestor) & box.size;
if (rect.contains(offset)) {
result = box.index;
if (widget.scrollPadding != null) {
box.showOnScreen(
rect: widget.scrollPadding.inflateRect(Offset.zero & box.size),
duration: const Duration(milliseconds: 150),
);
}
return;
}
} else if (result == -1) {
element.visitChildElements(visitChildren);
}
}
context.visitChildElements(visitChildren);
return result;
}
}
class _MultiSelectChild extends SingleChildRenderObjectWidget {
const _MultiSelectChild({
Key key,
@required this.index,
@required this.child,
}) : super(key: key);
final int index;
final Widget child;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderMultiSelectChild(index: index);
}
@override
void updateRenderObject(BuildContext context, _RenderMultiSelectChild renderBox) {
renderBox.index = index;
}
}
class _RenderMultiSelectChild extends RenderProxyBox {
_RenderMultiSelectChild({
RenderBox child,
@required this.index,
}) : assert(index != null),
super(child);
int index;
}
class MultiChildSelection {
const MultiChildSelection(this.total, this.start, this.end);
static const empty = MultiChildSelection(0, -1, -1);
final int total;
final int start;
final int end;
bool get selecting => total != 0;
@override
String toString() => 'MultiChildSelection{$total, $start, $end}';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment