I am trying to implement drag and drop between multiple listviews in flutter and am fairly a new comer to flutter.
Below is the demo video of what I am trying to do and what is actually happening:-
The basic idea is as follows:-
- The items will be dragabble between the listviews for this I have used the Draggable and DragTarget Widgets and this part works fine as can be seen in DragDropItem below.
- If the drag reaches top or bottom of the ListView the items will scroll automatically. For this I used Listener because it doesn't compete in the GestureArena so was the natural choice. But during my searches I came across a fact that Listener needs a TapDown Event to work which is true in the first Listview but not in the others.
- There is a second problem here - the print statements are printing out the position of the first ListView(the one where the drag originates). This leads me to conclude that the other contexts are inactive at the moment but I am not sure why.
- The listviews will be nested 3 levels deep. So, BoardView and DragDropLists can't be used.
I have implemented the code as follows -
- I have used a horizontal listview containing vertical listviews.
- I have created a widget named DragDropItem which is nothing but LongPressDraggable and DragTarget put together.
- I have put a listview wrapped with listener widget in a widget of its own to avoid having to use a global key (Global keys are not making any difference in any case).
- In the onMove callback of the listener I have put in some print statements to show what is going on.
I am listing below the codes:
// ignore_for_file: must_be_immutable
import 'package:draggable_listview/drag_drop_item.dart';
import 'package:flutter/material.dart';
class DragScrollableList extends StatelessWidget {
final ScrollController _controller = ScrollController();
bool _isDragging = false;
DragScrollableList({super.key});
@override
Widget build(BuildContext context) {
return Listener(
onPointerMove: (event) {
if (!_isDragging) {
print('Is not Dragging');
return;
}
print('Is Dragging');
var renderObject = context.findRenderObject() as RenderBox;
var size = renderObject.size;
var renderOffset = renderObject.localToGlobal(Offset.zero);
var dragOffset = renderObject.localToGlobal(event.localPosition);
if (dragOffset.dx > renderOffset.dx &&
dragOffset.dx < renderOffset.dx + size.width &&
dragOffset.dy > renderOffset.dy &&
dragOffset.dy < renderOffset.dy + size.height) {
_controller.animateTo(dragOffset.dy + 100,
duration: const Duration(milliseconds: 200), curve: Curves.ease);
}
print('ListView Size: $size');
print('ListView Offset: $renderOffset');
print('Drag Offset: $dragOffset');
},
child: ListView.builder(
controller: _controller,
itemCount: 30,
itemBuilder: (context, index) => DragDropItem(
onDragEnded: () => _isDragging = false,
onDragStarted: () => _isDragging = true,
value: 'Item $index',
child: ListTile(
title: Text('Item $index'),
),
),
),
);
}
}
import 'package:flutter/material.dart';
class DragDropItem<T extends Object> extends StatelessWidget {
final Widget _child;
final T _value;
final bool _vertical;
final VoidCallback? _onDragStarted;
final VoidCallback? _onDragEnded;
const DragDropItem({
super.key,
required Widget child,
required T value,
bool vertical = true,
VoidCallback? onDragStarted,
VoidCallback? onDragEnded,
}) : _onDragEnded = onDragEnded,
_onDragStarted = onDragStarted,
_vertical = vertical,
_child = child,
_value = value;
@override
Widget build(BuildContext context) {
Widget draggable = LayoutBuilder(builder: (context, constraints) {
return LongPressDraggable<T>(
data: _value,
maxSimultaneousDrags: 1,
childWhenDragging: Opacity(
opacity: 0.5,
child: _child,
),
feedback: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: constraints,
child: _child,
),
),
onDragCompleted: () => _dragEnded(),
onDragEnd: (details) => _dragEnded(),
onDragStarted: () => _dragStarted(),
// onDragUpdate: _onDragUpdate,
onDraggableCanceled: (velocity, offset) => _dragEnded(),
child: _child,
);
});
return DragTarget<T>(
builder: (
BuildContext context,
List<T?> candidateData,
List<dynamic> rejectedData,
) {
if (_vertical) {
return Column(
children: <Widget>[
AnimatedSize(
duration: const Duration(milliseconds: 100),
child: candidateData.isEmpty
? Container()
: Opacity(
opacity: 0.0,
child: _child,
),
),
candidateData.isEmpty ? draggable : _child
],
);
} else {
return Row(
children: <Widget>[
AnimatedSize(
duration: const Duration(milliseconds: 100),
child: candidateData.isEmpty
? Container()
: Opacity(
opacity: 0.0,
child: _child,
),
),
candidateData.isEmpty ? draggable : _child
],
);
}
},
onAccept: (data) => _dragEnded(),
// onAcceptWithDetails: _onAcceptWithDetails,
// onLeave: (data) => _dragEnded(),
// onMove: _onMove,
onWillAccept: (data) {
if (data != null && _value != data) {
return true;
}
return false;
},
);
}
void _dragEnded() {
if (_onDragEnded == null) {
return;
}
_onDragEnded!();
}
void _dragStarted() {
if (_onDragStarted == null) {
return;
}
_onDragStarted!();
}
}
import 'dart:ui';
import 'package:flutter/material.dart';
import 'drag_drop_item.dart';
import 'drag_scrollable_list.dart';
import 'draggable_list.dart';
void main() {
runApp(const MyApp());
}
class MyCustomScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
// etc.
};
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
scrollBehavior: MyCustomScrollBehavior(),
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView.builder(
itemCount: 5,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
return DragDropItem(
value: List.generate(30, (index) => 'Item $index'),
vertical: false,
child: SizedBox(
width: 300.0,
child: DragScrollableList(),
),
// child: const DraggableList(
// maxHeight: 2500.0,
// maxWidth: 350.0,
// ),
);
},
),
);
}
}
So, can I make the listener listen to pointer events even if it doesn't receive on tap down first? If not can I achieve my objective some other way?
Any help on the subject is appreciated. Thanks in advance.
