How to swipe/drag 2 or more buttons in a grid of buttons using flutter
You can manually hit test RenderBox and extract a specific RenderObject of your choice.
We could for example add the following renderobject above our buttons:
class Foo extends SingleChildRenderObjectWidget { final int index; Foo({Widget child, this.index, Key key}) : super(child: child, key: key); @override RenderObject createRenderObject(BuildContext context) { return _Foo()..index = index; } @override void updateRenderObject(BuildContext context, _Foo renderObject) { renderObject..index = index; }}class _Foo extends RenderProxyBox { int index;}
Then use a Listener to extract all _Foo
found under the pointer.
Here's a full application using this principle:
import 'package:flutter/gestures.dart';import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';void main() { runApp(MyApp());}class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue), home: Grid(), ); }}class Grid extends StatefulWidget { @override GridState createState() { return new GridState(); }}class GridState extends State<Grid> { final Set<int> selectedIndexes = Set<int>(); final key = GlobalKey(); final Set<_Foo> _trackTaped = Set<_Foo>(); _detectTapedItem(PointerEvent event) { final RenderBox box = key.currentContext.findRenderObject(); final result = BoxHitTestResult(); Offset local = box.globalToLocal(event.position); if (box.hitTest(result, position: local)) { for (final hit in result.path) { /// temporary variable so that the [is] allows access of [index] final target = hit.target; if (target is _Foo && !_trackTaped.contains(target)) { _trackTaped.add(target); _selectIndex(target.index); } } } } _selectIndex(int index) { setState(() { selectedIndexes.add(index); }); } @override Widget build(BuildContext context) { return Listener( onPointerDown: _detectTapedItem, onPointerMove: _detectTapedItem, onPointerUp: _clearSelection, child: GridView.builder( key: key, itemCount: 6, physics: NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 1.0, crossAxisSpacing: 5.0, mainAxisSpacing: 5.0, ), itemBuilder: (context, index) { return Foo( index: index, child: Container( color: selectedIndexes.contains(index) ? Colors.red : Colors.blue, ), ); }, ), ); } void _clearSelection(PointerUpEvent event) { _trackTaped.clear(); setState(() { selectedIndexes.clear(); }); }}class Foo extends SingleChildRenderObjectWidget { final int index; Foo({Widget child, this.index, Key key}) : super(child: child, key: key); @override _Foo createRenderObject(BuildContext context) { return _Foo()..index = index; } @override void updateRenderObject(BuildContext context, _Foo renderObject) { renderObject..index = index; }}class _Foo extends RenderProxyBox { int index;}
I don't like this code at all, but it seems to be working
import 'package:flutter/material.dart';class TestScaffold extends StatefulWidget { @override State<StatefulWidget> createState() => _TestScaffoldState();}List<_SquareButton> _selectedList = [];class _TestScaffoldState extends State<TestScaffold> { List<_SquareButton> buttons = [ _SquareButton('1'), _SquareButton('2'), _SquareButton('3'), _SquareButton('4'), _SquareButton('5'), _SquareButton('6'), _SquareButton('7'), _SquareButton('8'), _SquareButton('9'), _SquareButton('10'), _SquareButton('11'), _SquareButton('12'), _SquareButton('13'), _SquareButton('14'), _SquareButton('15'), _SquareButton('16'), ]; Map<Rect, _SquareButton> positions = {}; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Test'),), body: GestureDetector( onPanDown: (details) { checkGesture(details.globalPosition); }, onPanUpdate: (details) { checkGesture(details.globalPosition); }, child: GridView.count(crossAxisCount: 4, physics: NeverScrollableScrollPhysics(), children: buttons,),) ); } initPositions() { if (positions.isNotEmpty) return; buttons.forEach((btn) { RenderBox box = btn.bKey.currentContext.findRenderObject(); Offset start = box.localToGlobal(Offset.zero); Rect rect = Rect.fromLTWH(start.dx, start.dy, box.size.width, box.size.height); positions.addAll({rect: btn}); }); } checkGesture(Offset position) { initPositions(); positions.forEach((rect, btn) { if (rect.contains(position)) { if (!_selectedList.contains(btn)) { _selectedList.add(btn); btn.state.setState((){}); } } }); }}class _SquareButton extends StatefulWidget { _SquareButton(this.title); final String title; final GlobalKey bKey = GlobalKey(); State state; @override State<StatefulWidget> createState() { state = _SquareButtonState(); return state; }}class _SquareButtonState extends State<_SquareButton> { @override Widget build(BuildContext context) { return Padding(key: widget.bKey, padding: EdgeInsets.all(4.0), child: Container( color: _selectedList.contains(widget) ? Colors.tealAccent : Colors.teal, child: Text(widget.title), alignment: Alignment.center, ),); }}
There is a moment. If you enable scrolling - GestureDetector not always work on vertical movements