I am attempting to have a horizontal gallery of elements which probably extends beyond the edges of the view so it needs to be horizontally scrollable. I have this. (This is primarily for a desktop app)
The elements in the gallery should be draggable but only for vertical dragging so that the user can place them somewhere else.
I've tried various approaches including a listener. Below seems to get closest to what I need, however, the picture elements are draggable in all directions. What I would like is when the user starts dragging them in a horizontal direction, instead of them being draggable, the gesture/control passes to the parent listview. So basically user can drag horizontally to scroll, and vertically to pull elements out of the gallery.
With current code, the user can scroll the listview by clicking and dragging between the elements, it seems the gesture detector never calls the onHorizontalDragStart (or onHorizontalDragUpdate.
(I have also tried with two nested GestureDetectors, one around the listview, and one around the PictElementDisplay but that didn't seem to make much sense.)
class PictGalleryView extends StatelessWidget {
PictGalleryView({
Key? key,
required this.size,
}) : super(key: key);
final Size size;
final projectSettings = GetIt.I<ProjectSettings>();
ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return SizedBox(
height: size.height,
width: size.width,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: projectSettings.numPictElements,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.all(5),
child: Center(
child: GestureDetector(
onHorizontalDragStart: (details) {
dev.log('onHorizontalDragStart');
// this doesn't happen?
},
child: PictElementDisplay(
//this shouldn't be horizontally draggable but it is!
element: projectSettings.pictElementDataList[index],
size: Size(75, 60),
),
),
),
);
},
),
),
);
}
}
//...
class PictElementDisplay extends StatelessWidget {
PictElementDisplay({
Key? key,
required this.element,
required this.size,
}) : super(key: key);
final PictElementData element;
final Size size;
@override
Widget build(BuildContext context) {
return SizedBox.fromSize(
child: Draggable(
data: element,
feedback: Container(
height: size.height,
width: size.width,
decoration: const BoxDecoration(
color: Colors.green, //todo
),
child: Text('id: ${element.id.toString()}'),
),
child: Container(
height: size.height,
width: size.width,
decoration: const BoxDecoration(
color: Colors.red, //todo
),
child: Text('id: ${element.id.toString()}'),
),
),
);
}
}
(and ChatGPT doesn't seem to quite know how to do it either. :-) ). Thanks in advance.
The GestureDetector
has to be inside the Draggable
then it can be used to override the default behaviour. In this use-case, you can pass in the ScrollControler
associated with the parent ListView
as a parameter to be modified which can control the listview. If you want to use the PictElementDisplay
in different contexts and the override to only apply when it appears in the gallery, you can make the widget nullable and add logic only to change the behaviour when the scroll controller is present i.e.
class PictElementDisplay extends StatelessWidget {
PictElementDisplay({
required this.element,
required this.size,
this.scrollController,
});
final PictElementData element;
final Size size;
final ScrollController? scrollController;
@override
Widget build(BuildContext context) {
Widget listChild = Container(
height: size.height,
width: size.width,
decoration: const BoxDecoration(
color: Colors.red, //todo
),
child: Text('id: ${element.id.toString()}'),
);
Widget gestureWidget = GestureDetector(
onHorizontalDragUpdate: (details) {
// Pass the control to the parent ListView to handle horizontal scrolling
dev.log('onHorizontalDragUpdate ${details.toString()}');
scrollController?.jumpTo(scrollController!.offset - details.delta.dx);
},
onTap: () {
//E.g. add to currently active page without needing to drag it
dev.log('tapped ${element.id}');
},
child: listChild,
);
Widget draggableChildWidget =
(scrollController != null) ? gestureWidget : listChild;
return SizedBox.fromSize(
child: Draggable(
data: element,
feedback: Container(
height: size.height,
width: size.width,
decoration: const BoxDecoration(
color: Colors.green, //todo
),
child: Text('id: ${element.id.toString()}'),
),
child: draggableChildWidget,
),
);
}
}
and in the parent level
@override
Widget build(BuildContext context) {
return SizedBox(
height: widget.size.height,
width: widget.size.width,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: ListView.builder(
controller: scrollController,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: projectSettings.numPictElements,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
PictElementData element =
projectSettings.pictElementDataList[index];
Size size = Size(75, 60);
return Padding(
padding: const EdgeInsets.all(5),
child: Center(
child: PictElementDisplay(
element: element,
size: size,
// scrollController allows the widget to
// override its own horizontal dragging
scrollController: scrollController,
),
),
);
},
),
),
);
}