New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
InteractiveViewer should support mouse scrolling #64210
Comments
That sounds reasonable, thanks for opening this issue. Maybe if scaling is disabled via We should also think about mice that have directional scrolling, like Apple's mice. |
@justinmc That sounds very good! 👍🏽 Actually, just yesterday we also discovered the case of directional scrolling. Any trackpad usually allows scrolling in all directions and that would be a great fit :) Regarding your point of automatic panning on scroll: I think that this is very reasonable because it seems like it would be the natural choice. If we create an interactive viewer, we probably expect it to be interactive. |
Ah thanks for the Lucidchart example. It looks like they do panning with bidirectional scrolling by default, and holding down the option key + scrolling does zoom. Come to think of it, I think this is how photo editors like Gimp and Photoshop work on desktop too. I think we can do a |
I just copied the void _receivedPointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent) {
if (_gestureIsSupported(_GestureType.scale)) {
final childRenderBox =
_childKey.currentContext.findRenderObject() as RenderBox;
final childSize = childRenderBox.size;
final scaleChange = 1.0 - event.scrollDelta.dy / childSize.height;
if (scaleChange == 0.0) {
return;
}
final focalPointScene = _transformationController.toScene(
event.localPosition,
);
_transformationController.value = _matrixScale(
_transformationController.value,
scaleChange,
);
// After scaling, translate such that the event's position is at the
// same scene point before and after the scale.
final focalPointSceneScaled = _transformationController.toScene(
event.localPosition,
);
_transformationController.value = _matrixTranslate(
_transformationController.value,
focalPointSceneScaled - focalPointScene,
);
} else if (_gestureIsSupported(_GestureType.pan)) {
final childRenderBox =
_childKey.currentContext.findRenderObject() as RenderBox;
final childSize = childRenderBox.size;
final yScroll = -event.scrollDelta.dy;
final xScroll = -event.scrollDelta.dx;
_transformationController.value = _matrixTranslate(
_transformationController.value,
Offset(xScroll, yScroll),
);
}
}
} Scaleing takes precedence, but if that's disabled the widget works like a charm with mousepads or scrollwheels. |
It looks like a rotation feature is in the works (#57698); it'd be great if a rotation touch gesture on macOS and iPadOS could trigger that as well, as is done in native apps like macOS's Preview. On another note, for those on the dev channel, click to expand @Schwusch's solution ported to Flutter 1.26.0-17.1.pre.// Handle mousewheel scroll events.
void _receivedPointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent) {
widget.onInteractionStart?.call(
ScaleStartDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
),
);
if (_gestureIsSupported(_GestureType.scale)) {
// Ignore left and right scroll.
if (event.scrollDelta.dy == 0.0) {
return;
}
// In the Flutter engine, the mousewheel scrollDelta is hardcoded to 20 per scroll, while a trackpad scroll can be any amount.
// The calculation for scaleChange here was arbitrarily chosen to feel natural for both trackpads and mousewheels on all platforms.
final double scaleChange = math.exp(-event.scrollDelta.dy / 200);
final Offset focalPointScene = _transformationController!.toScene(
event.localPosition,
);
_transformationController!.value = _matrixScale(
_transformationController!.value,
scaleChange,
);
// After scaling, translate such that the event's position is at the
// same scene point before and after the scale.
final Offset focalPointSceneScaled = _transformationController!.toScene(
event.localPosition,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
focalPointSceneScaled - focalPointScene,
);
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
rotation: 0.0,
scale: scaleChange,
horizontalScale: 1.0,
verticalScale: 1.0,
));
widget.onInteractionEnd?.call(ScaleEndDetails());
} else {
widget.onInteractionEnd?.call(ScaleEndDetails());
if (_gestureIsSupported(_GestureType.pan)) {
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
Offset(-event.scrollDelta.dx, -event.scrollDelta.dy),
);
}
}
}
} |
@justinmc InteractiveViewer should support scrollbars, in both directions. Just like figma. Thanks! Example: |
@zhou67832033 Thanks, that's a great example of 2d mouse scrolling with scroll bars! |
Are there currently plans to support Scrollbar feature? |
Have you implemented scrollbars, can you share it? |
No one is currently working on scrollbars for InteractiveViewer that I'm aware of, but it seems like a good idea. |
@zhou67832033 I have not, sorry |
vertical scroll should work using the scrollWheel and then the zoom using control + scroll |
Yes precisely, this is rather straight-forward to implement. You can see an example I built in this demo. For devices without a trackpad, you can use shift + scroll for horizontal panning. For devices with trackpads, you simply use the 2D scroll. |
It would be nice if scrollbar is supported by InteractiveViewer. Currently, I have a some troubles when using nested scrollbars for the same purpose. |
This works for panning by scrolling: master...justinmc:iv-scroll-pan However, what are all of the use cases that we want to support here? Some people have mentioned that scrolling should only pan in the vertical direction and shift+scroll should pan horizontally, others seem to want slightly different behavior. Can we support everyone's use cases using an enum like in that branch? Or do we need something more powerful? |
Related: Another issue calls for pinch-to-zoom with the trackpad #63084 |
@justinmc having written support for this in Flutter myself, looking at other Flutter web apps like Rive (+ looking at their sources), and working with many apps supporting this (Miro, Lucidchart, etc.), this is the default behavior I have observed:
@justinmc tbh, you can probably just go to The behavior is not too complicated to implement perfectly. There is one blocker: Rive and I both had to override some default DOM behaviors (see my answers on SO, i.e. this one and this one). Also, you might want to add some "polyfill"-esque code to make sure the behavior is consistent across all browsers. You can check the Rive source for that. Overall, I think this is the expected behavior everywhere looking at all major apps and it is fairly trivial to get perfectly working with a bit of HTML event handling. I can share my implementation if that helps :) |
@justinmc Thank you for the class __FlowGraphState extends State<_FlowGraph> {
bool _isZoomKeyPressed = false;
@override
void initState() {
super.initState();
RawKeyboard.instance.addListener(_onKeyPressed);
}
void _onKeyPressed(RawKeyEvent event) {
var zoomKeyPressed = event.isControlPressed || event.isMetaPressed;
if (_isZoomKeyPressed != zoomKeyPressed) {
setState(() {
_isZoomKeyPressed = zoomKeyPressed;
});
}
}
@override
Widget build(BuildContext context) {
return InteractiveViewer(
scrollControls: _isZoomKeyPressed
? InteractiveViewerScrollControls.scrollScales
: InteractiveViewerScrollControls.scrollPans,
maxScale: 1.5,
minScale: 0.1,
constrained: false,
boundaryMargin: EdgeInsets.all(5000),
child: child,
);
}
@override
void dispose() {
RawKeyboard.instance.removeListener(_onKeyPressed);
super.dispose();
}
} The only thing I would change in the var currentScale = _transformationController!.value.getMaxScaleOnAxis();
final Offset translation = Offset(-event.scrollDelta.dx, -event.scrollDelta.dy) / currentScale; |
Currently the PointerScrollEvent does not send the implicit "ctrlKey' == true when the gesture is zooming vs panning. You can keep track of the control key separately with the keyboard listener. if (event is PointerScrollEvent) {
// TODO: Scale and Pan at the same time
if (_controlPressed) {
double zoomDelta = (-event.scrollDelta.dy / 300);
final scale = zoomDelta;
final origin = controller.mousePosition;
print("scale: ${scale} at origin: ${origin}");
} else {
final panDelta = -event.scrollDelta;
print("pan: ${panDelta}");
}
} You can capture this information correctly with dart:html and the control key is implicitly true when its a zoom gesture on the trackpad: html.document.body!.addEventListener('wheel', (e) {
e.preventDefault();
final event = e as html.WheelEvent;
final origin = event.offset;
if (e.ctrlKey == true) {
double scale = 1;
if (event.deltaY < 0) {
scale = 0.1;
} else {
scale = -0.1;
}
print("scale: ${scale} at origin: ${origin}");
} else {
final panDelta = Offset(-event.deltaX.toDouble(), -event.deltaY.toDouble());
print("pan: ${panDelta}");
}
}, false); Wheel Event: https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event You can pan and zoom with the same event with this demo: |
I recently looked into InteractiveViewer and making it a 2D scrollable under the hood, and unfortunately, that is not something we are likely to do. Retro-fitting this for scrolling would likely require a bunch of breaking changes to this widget. Regarding this issue though, there are a lot of comments with varying feature requests. I'd like to resolve these. Since there are several, we will likely need to split them out individual issues so we can get them assigned and addressed. From what I gather above:
Can anyone confirm some of this for us? With a clear picture of what is needed, we can set about having it done. :) Thanks! |
The code from @justinmc master...justinmc:iv-scroll-pan posted in #64210 (comment) is a good start.
I confirm that with the latest version on master |
Great! Thank for the feedback. I have updated the list above. |
I would like to point out that adding mouse scrolling to @Piinks Is the new widget you are planning to build meant to replace |
@creativecreatorormaybenot Mouse scrolling on some platforms (macOS notably) requires scrolling physics, and implementing that requires either the whole scrolling infrastructure or duplicating that whole logic, neither of which is reasonable to retrofit into a widget that wasn't built for it. Our goal is to have widgets be coherent and implement the same APIs so that they are easier to fit together. How exactly this will turn out for InteractiveViewer is yet to be determined. I recommend letting @Piinks work through the design and see what they come up with. In the meantime if you would be satisfied with a variant of InteractiveViewer that implements just part of the problem (e.g. non-physics-aware mouse scrolling) then you are welcome to fork the widget and upload a package to pub. |
I did the impossible... I found a way to both support mouse scrolling and showing both scrollbars using InteractiveViewer class. I made this package: flutterx_table |
I am removing my assignment because I am focused on a separate 2D scrolling widget (See https://github.com/orgs/flutter/projects/32), and @moffatman has been working on this specifically in the linked PRs above. :) |
It should also have an option to disable mouse wheel events. In my case I need interactive viewer for mobile devices but ** don't want to update scale for mouse wheel events**. Is there any way to disable it or do we need to write custom code ourselves? |
Now that #112171 and related PRs are merged, scrolling on a mouse or trackpad causes a pan, and pinching on a trackpad causes a scale. There are several other suggestions in this issue (like the previous comment), but I'm going to consider this issue complete. For any other related features/bugs, please create a new issue and tag me. For the option to allow scrolling to scale as before, see #114280. |
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of |
Use case
We use @justinmc's
InteractiveViewer
widget for vertical lists that can have an arbitrary width (the items can have arbitrary widths).→ you can obviously apply the same to the reverse dimensions :)
This is very nice for allowing this use case. However, what is a bummer that the mouse scrolling characteristic is lost due to this widget.
Users find this to be really unintuitive on web.
Proposal
The
InteractiveViewer
should have something like ascrollingDirection
attribute, which determines theAxis
along which a mouse scroll will scroll theInteractiveViewer
.In my example, this would allow to use the mouse scroll wheel (or trackpad scrolling) to scroll vertically while scrolling horizontally still needs a drag.
The text was updated successfully, but these errors were encountered: