Repository navigation
Pan and zoom gallery demo - #25164
Conversation
|
TODO:
|
03f0fc3 to
c9ec50e
Compare
0155eb6 to
204ea54
Compare
HansMuller
left a comment
There was a problem hiding this comment.
Mostly superficial feedback about the code.
| import 'pan_and_zoom_demo_transform_interaction.dart'; | ||
|
|
||
| class PanAndZoomDemo extends StatefulWidget { | ||
| const PanAndZoomDemo({Key key}) : super(key: key); |
| final BoardPainter painter = BoardPainter( | ||
| board: _board, | ||
| ); | ||
| final Size screenSize = MediaQuery.of(context).size; |
There was a problem hiding this comment.
We need to get away from doing this kind of thing because it doesn't work on tablets or desktops. Better to use a LayoutBuilder to build this widget and then pass the "screen" size along as a parameter to this widget.
There was a problem hiding this comment.
Sounds good, I wasn't aware of that pattern but I like it a lot more.
I may revisit the size and visibleRect params when I bring this widget into Flutter. It might be better if this is as big as it can be and users can constrain the size with a parent widget.
|
|
||
| final FloatingActionButton floatingActionButton = _board.selected == null | ||
| ? FloatingActionButton( | ||
| onPressed: () => setState(() { |
There was a problem hiding this comment.
indent two spaces here and below
We usually reserve the => notation for one-liners that return a value
| size: Size.infinite, | ||
| painter: painter, | ||
| ), | ||
| visibleRect: Rect.fromLTWH( |
There was a problem hiding this comment.
Maybe (someday) we should change the Rect.fromPoints parameters from Offset to OffsetBase so that you could write: visibleRect: Rect.fromPoints(-visibleSize / 2, visibleSize)
There was a problem hiding this comment.
That would work for me!
| // CustomPainter is what is passed to CustomPaint and actually draws the scene | ||
| // when its `paint` method is called. | ||
| class BoardPainter extends CustomPainter { | ||
| BoardPainter({ |
There was a problem hiding this comment.
Looks like should be const BorderPainter({ this.board }) etc
| // Return a new matrix representing the given matrix after applying the given | ||
| // translation. | ||
| Matrix4 matrixTranslate(Matrix4 matrix, Offset translation) { | ||
| if (widget.disableTranslation) { |
There was a problem hiding this comment.
asserts that prove the parameter values aren't insane might be useful here.
| } | ||
| void _onScaleUpdate(ScaleUpdateDetails details) { | ||
| widget.onScaleUpdate?.call(fromViewport(details.focalPoint, _transform)); | ||
| double scale = _transform.getMaxScaleOnAxis(); |
There was a problem hiding this comment.
Can scale be zero? That's not going to work below, where foo / scale
| // Return a new matrix representing the given matrix after applying the given | ||
| // scale transform. | ||
| Matrix4 matrixScale(Matrix4 matrix, double scale) { | ||
| if (widget.disableScale) { |
There was a problem hiding this comment.
Are we done if scale is zero?
| return matrix; | ||
| } | ||
|
|
||
| // Return the scene point underneath the viewport point given. |
There was a problem hiding this comment.
underneath the viewport point given. => at the given viewport point
| onDoubleTap: widget.onDoubleTap, | ||
| onLongPress: widget.onLongPress, | ||
| onLongPressUp: widget.onLongPressUp, | ||
| onVerticalDragDown: widget.onVerticalDragDown == null ? null : (DragDownDetails details) => widget.onVerticalDragDown(fromViewport(details.globalPosition - getOffset(context), _transform)), |
There was a problem hiding this comment.
It might be clearer to factor out a generic method that produces these closure values:
T _callbackValue<T>(T callback) {
if (callback == null)
return callback;
return (T details) => callback(fromViewport(details.globalPosition - getOffset(context), _transform));
}
And then:
onVerticalDragDown: callbackValue<DragDownDetails>(widget.onVerticalDragDown),
This is assuming that you can define a local generic method. Otherwise you'd have to pass along more parameters.
There was a problem hiding this comment.
I can't seem to get this to work. It won't let me write details.globalPosition when details is an unknown type. Unfortunately there is no common parent class or interface for all of these different Details objects.
When I first wrote this, I was also trying things like having TransformInteraction inherit from GestureDetector, but I couldn't get anything like that working either.
Any thing else to try?
There was a problem hiding this comment.
Sorry, here's a version that should work:
ValueChanged<T> _callbackValue<T>(ValueChanged<T> callback) {
if (callback == null)
return callback;
return (T details) => callback(fromViewport(details.globalPosition - getOffset(context), _transform));
}
There was a problem hiding this comment.
I need your help implementing this. These callbacks have changed a bit so it needs to be slightly different again. I'll show you later.
| static const double _kHexagonRadius = 32.0; | ||
| static const double _kHexagonMargin = 1.0; | ||
| static const int _kBorderRadius = 8; | ||
|
|
There was a problem hiding this comment.
add code comment for what this is
| board: _board, | ||
| ); | ||
| final FloatingActionButton floatingActionButton = _board.selected == null | ||
| ? FloatingActionButton( |
There was a problem hiding this comment.
The reader can realize there are 2 alternative FABs but can't tell what the intents are. It's also a bit hard to read. Put them into small functions and give them self-documenting names that describe their purposes.
| // The scene is drawn by a CustomPaint, but user interaction is handled by | ||
| // the TransformInteraction parent widget. | ||
| return Scaffold( | ||
| appBar: AppBar(), |
There was a problem hiding this comment.
somewhat surprised you don't need a title here
There was a problem hiding this comment.
I guess not. I only added this because some smoke tests look for an app bar with a back button. Maybe I should add a title...
| body: LayoutBuilder( | ||
| builder: (BuildContext context, BoxConstraints constraints) { | ||
| final Size size = Size(constraints.maxWidth, constraints.maxHeight); | ||
| final Size visibleSize = Size(size.width * 3, size.height * 2); |
There was a problem hiding this comment.
This line needs a comment. I assume this is just an arbitrary size of the board you're defining?
There was a problem hiding this comment.
Yeah, this is how much the user can see beyond the size of the widget by panning around before hitting a "wall". This size just feels nice for the demo. I'll add a comment.
| @@ -10,6 +10,7 @@ export 'cupertino/cupertino.dart'; | |||
| export 'fortnightly/fortnightly.dart'; | |||
There was a problem hiding this comment.
what's the file above? is it intentional?
There was a problem hiding this comment.
Fortnightly is a news app demo in the gallery. I didn't change anything about it.
There was a problem hiding this comment.
Not fortnightly, I can't comment on the file but it's default.profraw
There was a problem hiding this comment.
Oh got it. Good catch! I must have accidentally added it. Will remove.
| final Widget child; | ||
| final Size size; | ||
| final bool reset; | ||
| final Function onTapDown; |
There was a problem hiding this comment.
Reuse the signatures of the recognizers instead of blank functions.
| this.onScaleEnd, | ||
| }) : | ||
| assert(!reset || onResetEnd != null, 'Must implement onResetEnd to use reset.'); | ||
|
|
There was a problem hiding this comment.
What does null min/max scales, disableTranslations, initialTranslations etc etc mean?
| @override TransformInteractionState createState() => TransformInteractionState(); | ||
| } | ||
|
|
||
| enum GestureType { |
There was a problem hiding this comment.
Why is this public? Document what this is for.
| rotate, | ||
| } | ||
|
|
||
| class TransformInteractionState extends State<TransformInteraction> with TickerProviderStateMixin { |
There was a problem hiding this comment.
Why is this public? Who is expected to call its APIs? Document them
There was a problem hiding this comment.
I just realized that I made this public so that I can access it in a unit test. Is that valid idea or is there a better way?
| } | ||
| }); | ||
| } | ||
| void _onScaleEnd(ScaleEndDetails details) { |
…ranslation at boundary (not ideal)
eae6e10 to
a3fd435
Compare
|
Ready for rereview. @HansMuller one outstanding thing: I need some help using your And @xster: See comment #25164 (comment) |
|
You can make the State private and grab it via "dynamic". If you make the
State public you are committing to that as a public API.
…On Mon, Apr 1, 2019, 14:29 Justin McCandless ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In
examples/flutter_gallery/lib/demo/pan_and_zoom_demo_transform_interaction.dart
<#25164 (comment)>:
> + final bool disableScale;
+ final bool disableRotation;
+ final Offset initialTranslation;
+ final double initialScale;
+ final double initialRotation;
+
+ @OverRide TransformInteractionState createState() => TransformInteractionState();
+}
+
+enum GestureType {
+ translate,
+ scale,
+ rotate,
+}
+
+class TransformInteractionState extends State<TransformInteraction> with TickerProviderStateMixin {
I just realized that I made this public so that I can access it in a unit
test. Is that valid idea or is there a better way?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#25164 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAhpHNP87xt3eb7MSbWn6wCsWwb12n4Kks5vcnpKgaJpZM4ZL-pn>
.
|
HansMuller
left a comment
There was a problem hiding this comment.
This is really close; just a handful of trivial feedback and a suggestion about factoring GestureTransformableState
| if (q <= 0) { | ||
| rStart = -boardRadius - q; | ||
| rEnd = boardRadius; | ||
| } else if (q > 0) { |
There was a problem hiding this comment.
I think this is just "else"
| @required this.selected, | ||
| this.onTap, | ||
| }) : assert(color != null), | ||
| assert(selected != null); |
| @required this.boardPoint, | ||
| this.onColorSelection, | ||
| }) : assert(boardPoint != null), | ||
| super(key: key); |
| import 'transformations_demo_inertial_motion.dart'; | ||
|
|
||
| // Empty function type for the onResetEnd parameter. | ||
| typedef _ResetEndCallback = void Function(); |
There was a problem hiding this comment.
This isn't needed, you can use VoidCallback
There was a problem hiding this comment.
Thanks, I didn't know about that.
| this.onScaleStart, | ||
| this.onScaleUpdate, | ||
| this.onScaleEnd, | ||
| }) : |
There was a problem hiding this comment.
Sadly, conventional formatting for the asserts is:
....
this.onScaleEnd,
}) : assert(child != null),
assert(size != null),
....
There was a problem hiding this comment.
I really had trouble with my assert indentation in this PR...
| // original position of the child. | ||
| @immutable | ||
| class GestureTransformable extends StatefulWidget { | ||
| const GestureTransformable({ |
There was a problem hiding this comment.
Widget classes should usually start with a Key key named parameter that's passed along via super(key: key)
| } | ||
|
|
||
| // Get the offset of the current widget from the global screen coordinates. | ||
| static Offset getOffset(BuildContext context) { |
There was a problem hiding this comment.
Eventually we'll have to explain when it's safe to call this (not at build time).
There was a problem hiding this comment.
At build time you wouldn't have a context, so is that enough discouragement from calling this at the wrong time?
There was a problem hiding this comment.
At build(BuildContext context) time is exactly when you might be tempted to call it.
At the moment this is just a private entry point within a demo. No need to worry about this yet.
There was a problem hiding this comment.
Oh I see what you mean! I'll add a TODO so I remember this later.
| // Translation is reversed (a positive translation moves the scene to the | ||
| // right, viewport to the left). | ||
| final Rect translationBoundaries = Rect.fromLTRB( | ||
| viewportBoundaries.right * -1 * scale, |
There was a problem hiding this comment.
Maybe:
-scale * viewportBoundaries.right,
etc
| import 'package:flutter/material.dart'; | ||
| import 'package:vector_math/vector_math.dart' show Vector2; | ||
|
|
||
| // Provides calculations for an object moving with inertia and friction using |
There was a problem hiding this comment.
Eventually a version of this should probably exist in the physics library.
There was a problem hiding this comment.
Now that you mention it I see we have friction_simulation.dart. I could probably reuse or extend that.
|
|
||
| @override | ||
| Widget build (BuildContext context) { | ||
| if (widget.reset && _animationReset == null) { |
There was a problem hiding this comment.
This looks like work that should be done at initStateTime() and in didUpdateWidget() (when the widget.reset flag changes).
… these by moving function to another class
98c71ae to
89e38ab
Compare
a139c52 to
1375d67
Compare
| _controllerReset = AnimationController( | ||
| vsync: this, | ||
| ); | ||
| if (widget.reset && _animationReset == null) { |
There was a problem hiding this comment.
This method only runs once when the State is constructed so _animationReset has to be null.
Adding a demo to the flutter_gallery app that shows how to do some basic canvas operations like panning, pinch to zoom, and hit detection.
Relevant issues: #8272, #12878
This PR won't close those issues, but after this goes in I'll open a new PR to move my code into a reusable widget that is more like what people are asking for.