I am trying to make a Widget that acts exactly like the Google Calendar Week View. That means
- Pinch to Zoom
- Scroll Vertically
Here is an example.
And here is the GitHub repository of the example.
For the purpose of simplicity I want to focus on just vertical scrolling being possible, I can add the rest myself.
The problem is that the PinchToZoom thing is very unreliable and often even though I am pinch-zooming, the lists begin to scroll. Why is this happening? I did some research and found this article.
It basically describes a simplified version of my problem, that is two GestureDetectors competing. The solution is a RawGestureDetector. I wrote my own:
class PinchToZoomGestureRecognizer extends OneSequenceGestureRecognizer {
final void Function() onScaleStart;
final void Function() onScaleUpdate;
final void Function() onScaleEnd;
PinchToZoomGestureRecognizer({
required this.onScaleStart,
required this.onScaleUpdate,
required this.onScaleEnd,
});
@override
String get debugDescription => '$runtimeType';
Map<int, Offset> pointerPositionMap = {};
@override
void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer);
pointerPositionMap[event.pointer] = event.position;
if (pointerPositionMap.length >= 2) {
resolve(GestureDisposition.accepted);
}
}
@override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
pointerPositionMap[event.pointer] = event.position;
return;
} else if (event is PointerDownEvent) {
pointerPositionMap[event.pointer] = event.position;
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
stopTrackingPointer(event.pointer);
pointerPositionMap.remove(event.pointer);
}
if (pointerPositionMap.length >= 2) {
resolve(GestureDisposition.accepted);
}
}
@override
void didStopTrackingLastPointer(int pointer) {
resolve(GestureDisposition.rejected);
}
}
All this is supposed to do is if there are two pointers or more, declare Victory in the GestureArena. This works fine if both pointers are entering the arena at the same time. However, if they are not, and the first pointer is accepted by the ListView, for the second pointer my GestureDetector is not even added to the Arena:
I/flutter (13717): Gesture arena 8 ❙ ★ Opening new gesture arena.
I/flutter (13717): Gesture arena 8 ❙ Adding: PinchToZoomGestureRecognizer#f9b77
I/flutter (13717): Gesture arena 8 ❙ Adding: VerticalDragGestureRecognizer#07525(start behavior: start)
I/flutter (13717): Gesture arena 8 ❙ Closing with 2 members.
I/flutter (13717): Gesture arena 8 ❙ Accepting: VerticalDragGestureRecognizer#07525(start behavior: start)
I/flutter (13717): Gesture arena 8 ❙ Self-declared winner: VerticalDragGestureRecognizer#07525(start behavior: start)
D/EGL_emulation(13717): app_time_stats: avg=781.66ms min=5.34ms max=12373.45ms count=16
I/flutter (13717): Gesture arena 9 ❙ ★ Opening new gesture arena.
I/flutter (13717): Gesture arena 9 ❙ Adding: VerticalDragGestureRecognizer#07525(start behavior: start)
I/flutter (13717): Gesture arena 9 ❙ Accepting: VerticalDragGestureRecognizer#07525(start behavior: start)
I/flutter (13717): Gesture arena 9 ❙ Closing with 1 member.
I/flutter (13717): Gesture arena 9 ❙ Default winner: VerticalDragGestureRecognizer#07525(start behavior: start)
This means, if the user doesnt perfectly time their two fingers to pinch-zoom, the list will simply scroll.
Here is the Widget Code I used to test this:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
debugPrintGestureArenaDiagnostics = true;
runApp(const MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
PinchToZoomGestureRecognizer: GestureRecognizerFactoryWithHandlers<
PinchToZoomGestureRecognizer>(
() => PinchToZoomGestureRecognizer(
onScaleStart: () {},
onScaleUpdate: () {},
onScaleEnd: () {},
),
(instance) {},
),
},
child: Container(
width: MediaQuery.of(context).size.width,
decoration: const BoxDecoration(
color: Colors.blueGrey,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(
100,
(index) => Text("Item $index"),
),
),
),
),
),
);
}
}
Note that I didn't actually implement pinch-zooming because that is trivial, its just about getting that GestureDetector to recognize the Gesture in the first place.
If anyone can help me with either a completely different approach for pinch-zooming a ListView or knows how the RawGestureDetector works and why in this case it doesnt work, I would really appreciate it.
Any idea how to make a reliable pinch-to-zoom widget?
CodePudding user response:
I used a gesture recognizer with EagerGestureRecognizer inside GoogleMap when I had a map in side a list and map supported pinch gestures see if this helps
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
Factory<OneSequenceGestureRecognizer>(
() => EagerGestureRecognizer(),
),
].toSet(),
CodePudding user response:
For pinch-to-zoom functionality you can use InteractiveViewer widget that allows you to recognize the scale event, setup min/max scale factors and etc
Example of simple usage:
InteractiveViewer(
minScale: 0.1,
maxScale: 2.0,
onInteractionStart: (details) {},
onInteractionUpdate: (details) {},
onInteractionEnd: (details) {},
child: widget,
)