Repository navigation
InteractiveViewer Widget - #56409
InteractiveViewer Widget#56409
Conversation
5cc1994 to
4701c6e
Compare
There was a problem hiding this comment.
Should I even bother using InteractiveViewer in these two gallery demos anymore now that the main gallery is a separate repo? I guess these are just for integration tests.
|
Notes for reviewers at this time:
|
fb9f624 to
d407d46
Compare
|
@justinmc - please assume that I've inserted "IMHO" below
Making the "out of the box" use case work nicely sounds like the right way to go.
Although I'm a big fan of low-cal APIs, providing these listeners seems like a useful convenience. On the other hand, they don't seem necessary for most apps? If they're typically not needed, since the InteractiveViewer's child will handle events directly, I think it would be best to just provide an easy way to do the transforms with the transformationController. |
I think it would be reasonable to ship the first version of this widget without rotation support. The docs will need to be updated to reflect that. |
HansMuller
left a comment
There was a problem hiding this comment.
I haven't really made it into the depth of this PR. Just submitting a few comments I made this morning, since it will be a little while before I return to it.
| /// that the callbacks are called with are untransformed and in relation to the | ||
| /// original position of the child. | ||
| /// | ||
| /// [child] must not be null. |
| import 'layout_builder.dart'; | ||
| import 'ticker_provider.dart'; | ||
|
|
||
| /// A widget that enables pan, zoom, and rotate interactions with its child. |
There was a problem hiding this comment.
Here and elsewhere: just leave rotation out of the API and docs.
| /// available. | ||
| /// | ||
| /// {@tool dartpad --template=stateful_widget_material_ticker} | ||
| /// This example shows how to use InteractiveViewer in the simple case of |
There was a problem hiding this comment.
This doesn't seem like a simple case because DragTarget. The examples of the PR's description seem like what you'd want here.
There was a problem hiding this comment.
Whoops I forgot I had left DragTarget in here. I'll simplify it to be like the first example in the description.
| /// column: const FixedColumnWidth(300.0), | ||
| /// }, | ||
| /// children: <TableRow>[ | ||
| /// for (int row = 0; row < rowCount; row++) |
There was a problem hiding this comment.
+= 1 instead of ++ because Flutter style
| /// panning over a large widget representing a table. | ||
| /// | ||
| /// ```dart | ||
| /// static Widget _getTable(int rowCount, int columnCount) { |
There was a problem hiding this comment.
With a few more lines of code this could be a stateless widget. Widgets are preferable to static methods.
| this.onVerticalDragEnd, | ||
| this.onVerticalDragStart, | ||
| this.onVerticalDragUpdate, | ||
| ValueNotifier<Matrix4> transformationController, |
There was a problem hiding this comment.
If this was its own type it would be easier to add functionality in the future. Or convenience methods for doing the coordinate transforms now.
|
|
||
| /// The Widget to perform the transformations on. | ||
| /// | ||
| /// [child] should usually have an intrinsic size. This is used to calculate |
There was a problem hiding this comment.
This needs some elaboration. When can child not have an intrinsic size? What's an example of a child with an intrinsic size? What's an example of a child that does not?
There was a problem hiding this comment.
I've since updated the widget so that it handles both cases just like any other widget would. For child: Container(width: 100, height: 100), you'll get a 100x100 box. For child: Container(), it can expand to the size of the parent, like it would if there was no InteractiveViewer. Things like boundary work intuitively in both cases.
I'll update these docs.
|
@not-holar You seem to be right about both of these. Here's a PR to reverse scroll zoom direction: #60523 And here's a PR to update the boundaries when child size changes: #60532 That second PR seems to greatly improve behavior after the screen size changes. I imagine there could still be edge cases that don't feel right though. Let me know if you see anything else. |
|
@justinmc would you mind taking a look at scale and pan performance on web / mobile web, especially when text is used? May even be an opportunity for a test/benchmark. I used statsfl (https://pub.dev/packages/statsfl) (to easily benchmark across mobile and desktop web browsers), your above column/row example, and added an appbar to increase the row/column count and disable/enable text. On chrome mobile, LG G7 ThinQ, issues are immediately apparent with text enabled and a crash can instantly be induced by scaling. FPS issues with scroll and scale start around 75x75 if text is disabled, jank is severe around 150x150, and crash prone around 300x300. On chrome with my decently specced PC, an FPS drop can be seen immediately when scrolling with text enable, but scaling can maintain 60fps. At 10x10 scaling performance is impacted, severe at 20x20, and crash prone on anything above 30x30. Without text performance issues start at 20x20, but the issue does not become severe until over 100x100.
This may be related to deeper issues with web, but even testing this with skia, you will still notice performance issues. If it is a deeper issue with web, maybe InteractiveViewer can be a better window to diagnose than previous widgets. |
|
@michaelreed Would you be able to open a separate issue with these details and the full code for your example? I appreciate you taking the time to dig into the performance. I haven't done much optimization for web so there could be some easy improvements to make. Long term though, I think we're going to need something like and Edit: Or maybe this is covered by #59775 |
|
Opened #60550 to cover. The planned builder will help when the user is zoomed, the general concept of a builder here reminds me of Occlusion Culling (https://docs.unity3d.com/Manual/OcclusionCulling.html). It may not help when the user is completely zoomed out though as it will still need to render a significant number of cells and the viewer can be customized to allow for large views, but prevent max scale thus still allowing scroll/pan. ListViews and GridViews have it easier and don't have to tackle the scale aspect. Although skia fixes the crashing related to text and increases performance significantly, I am still seeing fps drop and jank when using it, especially on mobile. If there's anything low hanging for performance on the widget side that you could change, that would be awesome. using edit: I believe the repaint issue to be separate. I can't think of any other widget that impacts scale like the InteractiveViewer, and it may have uncovered/be connected to issues with how flutter handles scaling. A bit over my head currently, but I have a hunch. |
|
Thanks for opening that issue, I'll try to see if there are any quick improvements to make. One thing I've been thinking about recently related to the scale problem. Currently, as a user of InteractiveViewer, you could listen to the current zoom level by adding a listener to transformationController and looking at |
|
Ah very cool, I can use that to pause animations. I could also implement an LOD system based on this to downscale assets. Great idea, let me know if any others come to mind. |
|
I am really looking forward to using this widget in one of my projects, do you know when it would be available in beta or stable? |
|
This made it into v1.19.0-5.0.pre, so it's only in master right now. It looks like it should be in the next beta release, though. |
InteractiveViewer Widget
|
Is it somehow possible to set the current scale or offset of the InteractiveViewer from outside the widget? Eg. by a GlobalKey etc. This is useful if you would like to implement a scroll bar or a zoom slider outside of the widget. |
|
Yes, using transformationController. There's an example in the docs there that should get you started, but you can set the matrix like |
|
Thank you, @justinmc. For future people having the same issue: transformationController.value = transformationController.value.scaled(myCustomScale); |
|
Will there be a way to allow other widgets to stay on top of the |
|
@aytunch What do you mean exactly? Here I can put something on top of InteractiveViewer using a Stack, but I think you're asking for something more complicated: https://dartpad.dartlang.org/fc0095e229e2ea1f8df7d06cd8e76dbe |
|
@justinmc I thought that once the Edit: One confusion I have with naming is why do we have only Also this example does not print anything when I am pinch zooming on the dartweb by the touchpad. |
|
@aytunch Glad that helped. I'm not sure about the error, hopefully it's just a problem with dartpad. I don't get the error when running locally in Chrome. Internally, scale, pan, and rotation are all implemented using GestureDetector's onScale callbacks. The given Pinch to zoom on a trackpad doesn't work right now, see #63060. Two finger scroll to zoom should work. |
|
@TerrenceAddison Thank you for the issue! I will respond over there. |
|
I think a method for panning to a certain point is going to be required if you're making a game like myself. If I have a large map and start panning around, how do I get back to where let's say my character is currently standing? I don't see any way of currently doing this in the widget's API. @justinmc |
|
@devteam-tchs just assign a new, or update the existing transformation controller. _transformController = new TransformationController(
Matrix4.translation(vm.Vector3(-2200, -500, 0)));``` |
|
+1 to @MichealReed's answer. You can also animate that by using Flutter's existing animation framework, which I have an example of in the gallery. |
|
@justinmc Where can I find the source code for your demos? The snippets provided aren't very useful without their full context. I've been trying to get the |
|
|
@MichealReed That seemed to have done it. Weird I thought I added that argument yesterday when I copied over this example https://github.com/justinmc/flutter-interactive-viewer-demos/blob/master/lib/table/table_page.dart. You are right that it needs to be added to the docs. |
|
I've opened a PR to improve the docs a bit around this, let me know what you guys think: #67314 And it seems like you've found my examples repository. I did a little bit of clean up and modernization there as well just now. |

Description
This PR introduces a new widget called InteractiveViewer. Any widget tree passed as its child will be able to be panned and zoomed by user gestures.
Closes #8272
Closes #12878
Closes #20175
Future: rotation #57698
Future: overscroll #58042
Future: direction locking: #58043
Future: builder #58603
Table Example
Image viewer example
This example was modified from the Flutter gallery.
Game board example
This example was also modified from the Flutter gallery.
API Details
boundaryMargin
The boundary is the invisible perimeter around the child that prevents the viewport from viewing beyond it. It's specified with
EdgeInsets. The default isEdgeInsets.zero, which prevents the viewport from viewing beyond the child. A positive value extends the boundary by the given amount. The example below allows the viewport to move 20px beyond the edge of the child in all directions.I used this API because I anticipate that users will most often want the boundary to perfectly frame the child, and otherwise will want to specify it in relation to the edges of the child. In these cases, the user doesn't have to know the size of the child or the current constraints in order to specify the boundary.
transformationController
transformationController is a ValueNotifier on the Matrix4 that defines the current transformation. It enables both listening to changes and setting the value of the matrix. This provides a generic way of implementing lots of new capabilities, like:
pan/zoom/(rotate)Enabled
Booleans that enable/disable their corresponding interaction.
min/maxScale
Doubles that limit zoom. Defaults to 0.8 and 2.5, respectively, just because I thought those values worked out well.
Question: Should I default them both to 1.0 so that it doesn't zoom at all, or is that unnecessarily difficult for the user?
GestureDetector listeners
I have passed through all callbacks for GestureDetector events, like onDoubleTap, etc. so that the user can receive events transformed back to the original child's coordinates. For example, without this, tapping the top left corner of the child is detected as 0,0 and translating the child to the bottom right of the viewport at tapping the child's top left corner is detected as viewportWidth,viewportHeight. My built-in onTapUp will do the inverse transform and return 0,0 for both taps.
Question: Should I remove all of these listeners and document that the user needs to handle this themselves? They could use a transformationController to get the current matrix and do the inverse transform themselves.Done, and addedtoScenehelper method on transformationController.Tests
More questions
Mouse zoom support? (PointerScrollEvent)DoneDrag targets inside of the child don't work if they are initially outside of the viewport.Fixed