-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Description
Use case
If you want to support both zooming and scaling on a widget you need to handle all pointer events on that widget though a ScaleGestureRecognizer. From the docs:
Pan and scale callbacks cannot be used simultaneously because scale is a superset of pan. Use the scale callbacks instead.
In order to disambiguate scale and pan gestures, you can use the pointerCount, which was added in #73474. You use it like this:
onScaleUpdate: (ScaleUpdateDetails details) {
if (details.pointerCount == 1) {
print("pan");
} else {
print("scale");
}
}
This works for finger pointers. But for trackpads, pointerCount is 2 for both zooming and panning, so this does not work. Instead, you need to do something like this:
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
///keeps record of the state of a trackpad. Set to none if
///the PointerDeviceKind is not a trackpad
enum TrackPadState { none, waiting, pan, scale }
class ScalePan extends StatefulWidget {
const ScalePan({
super.key,
});
@override
State<ScalePan> createState() => _ScalePanState();
}
class _ScalePanState extends State<ScalePan> {
///Used for trackpad pointerEvents to determine if the user is panning or scaling
late TrackPadState _trackPadState;
///Total distance the trackpad has moved vertically since the last scale start event
Size _globalTrackpadDistance = Size.zero;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onScaleStart: (ScaleStartDetails details) {
_trackPadState =
event.kind == PointerDeviceKind.trackpad ? TrackPadState.waiting : TrackPadState.none;
},
onScaleUpdate: (ScaleUpdateDetails details) {
//If the trackpad has not moved enough to determine the
//gesture type, then wait for it to move more
if (_trackPadState == TrackPadState.waiting) {
if (details.scale != 1.0) {
_trackPadState = TrackPadState.scale;
print("recognized trackpad scale");
} else {
_globalTrackpadDistance += details.focalPointDelta;
if (_globalTrackpadDistance.longestSide > kPrecisePointerPanSlop) {
_trackPadState = TrackPadState.pan;
print("recognized trackpad pan");
}
}
}
if (details.pointerCount > 1 && _trackPadState == TrackPadState.none ||
_trackPadState == TrackPadState.scale) {
print("scale");
} else if (_trackPadState != TrackPadState.waiting) {
print("pan");
}
},
onScaleEnd: (ScaleEndDetails details) {
_trackPadState = TrackPadState.none;
_globalTrackpadDistance = Size.zero;
},
);
}
}
This is rather non obvious and a lot of boilerplate. You might try to just assume if scale == 1.0 then it's a pan but I don't think that works because scales start with scale == 1.0 (I think) and can go back to 1.0 if you scale up and then scale back down passed 1.0 or vice versa.
Edit: This might be a bit nicer but also slower(?)
code
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
///keeps record of the state of a scale gesture.
enum PanScaleState { waiting, pan, scale }
class ScalePan extends StatefulWidget {
const ScalePan({
super.key,
});
@override
State<ScalePan> createState() => _ScalePanState();
}
class _ScalePanState extends State<ScalePan> {
///Used for pointerEvents to determine if the user is panning or scaling
late PanScaleState _panScaleState;
///Total distance the trackpad has moved vertically since the last scale start event
Size _globalTrackpadDistance = Size.zero;
late PointerDeviceKind kind;
void _checkScalePanType(ScaleUpdateDetails details){
if(kind == PointerDeviceKind.trackpad){
if (_panScaleState == PanScaleState.waiting) {
if (details.scale != 1.0) {
_panScaleState = PanScaleState.scale;
print("recognized trackpad scale");
} else {
_globalTrackpadDistance += details.focalPointDelta;
if (_globalTrackpadDistance.longestSide > kPrecisePointerPanSlop) {
_panScaleState = PanScaleState.pan;
print("recognized trackpad pan");
}
}
}
}else{
if (details.pointerCount == 1){
_panScaleState = PanScaleState.pan;
}else{
_panScaleState = PanScaleState.scale;
}
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onScaleStart: (ScaleStartDetails details) {
kind = event.kind;
if(kind == PointerDeviceKind.trackpad){
_panScaleState = PanScaleState.waiting;
}
_globalTrackpadDistance = Size.zero;
},
onScaleUpdate: (ScaleUpdateDetails details) {
_checkScalePanType(details);
if (_panScaleState == PanScaleState.scale) {
print("scale");
}
if (_panScaleState == PanScaleState.pan) {
print("pan");
}
},
onScaleEnd: (ScaleEndDetails details) {
},
);
}
}
Here is a related issue that I think can be closed if this was fixed: #13102.
Proposal
If possible, Flutter should provide a way to handle this natively. However, I can't really think of a good way to do it. Here are some bad ways:
- Add a new field to
ScaleUpdateDetailscalledisPanthat returns true when the distance of panSlop of the deviceKind is reached.ScaleUpdateDetailsalraedy has too many fields in my opinion. This seems like a hack. - Provide a utility class that keeps track of the scale update events. You would feed it
ScaleUpdateDetialsand it would return whether the gesture is a scale, pan or waiting. This is a bit better but doesn't seem like Flutter style.
I'm not sure what a good API would look like for this. I'm also unclear about the performance cost of calling into a function or class on every scale update to check if it's a pan or scale. onScaleUpdate is called pretty frequently. If there is no good API for this than we should not support it.
Another point to consider is that the behavior of pointer panning and scaling is generally different from trackpad panning and scaling in that with pointer pan/scales, if you start the gesture with a pan you should be able to switch to scale without ending the gesture (ie. without releasing the pointer) and vice versa. From some testing with trackpad scaling on some other desktop applications that isn't the case with trackpads. Once you start zooming, the gesture remains a zoom until a pointer is lifted from the trackpad. So if Flutter handles this, it couldn't just be a general pan detection feature for scales, it would need to be specific to trackpads.