Repository navigation
Conversation
… approach, with axis-aligned boundary, depends on size of viewport, but I didn't realize that before
…dge when rotated, but gets weird numbers in corners and parallel...
…, though there is probably a solution that corrects cases with two invalid sides
|
|
||
| /// Returns the closest point to the given point on the given line segment. | ||
| @visibleForTesting | ||
| static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) { |
There was a problem hiding this comment.
These methods that I removed were involved in my previous iteration of correcting an invalid matrix, which in this PR I have generalized to work with a rotated matrix as well.
|
|
||
| return _round(largestExcess); | ||
| } | ||
| const double _kTolerance = 100000000000.0; |
There was a problem hiding this comment.
There are a bunch of places where I had to use rounding in order to get numbers to match. Do we have a standardized way of handling stuff like this somewhere?
There was a problem hiding this comment.
get numbers to match
You mean checking for floating-point equality/comparison? As far as I can tell we just check if 2 floating numbers are within an epsilon range, eg:
flutter/packages/flutter/lib/src/physics/utils.dart
Lines 5 to 21 in 02d441e
|
I think the biggest open questions for this PR are:
|
|
CC @RomanJos in case you're interested in reviewing this. |
|
Hi, thanks for calling me I appreciate it. My understanding of how this widget handle gestures is as follow :
I have two problems with this logic :
I believe it would be nicer to transform This would solve both problem as the focal point will move with the child. During testing I also found some issue involving
doc_2021-02-06_15-41-38.mp4I hope you understood everything, I borrowed a phone from a friend as I don't have one currently so I couldn't test more lol |
| const LineSegment( | ||
| this.p0, | ||
| this.p1, | ||
| ); |
There was a problem hiding this comment.
assert p0 != p1 maybe?
There was a problem hiding this comment.
Actually this seems to make LineSegment not be able to be const anymore. Is there a way around that?
| /// A special case of the linesIntersectAt method. | ||
| static Offset _isVerticalAndInterceptsNonVerticalLineAt(LineSegment vertical, LineSegment nonVertical) { | ||
| // Assert that vertical is indeed vertical. | ||
| assert(vertical.p0.dx == vertical.p1.dx); |
There was a problem hiding this comment.
nit: asserts nonVertical is not vertical too maybe?
| } | ||
| final double d0 = _distanceBetweenPoints(pointOnLine, p0); | ||
| final double d1 = _distanceBetweenPoints(pointOnLine, p1); | ||
| return d0.abs() <= d1.abs() ? p0 : p1; |
There was a problem hiding this comment.
isn't the distance always non-negative?
| ); | ||
| } | ||
|
|
||
| // Otherwise, the closest points on the line segments are the closest points |
There was a problem hiding this comment.
Is this true? If the 2 lines are not perpendicular, and the intersection is only on one of the line segments, then the closet point on that line segment is not necessarily the intersection right?
e.g.:
_________
\
\
the closest point on the horizontal line is not the (projected) intersection.
There was a problem hiding this comment.
You're right, thanks for catching this! I feel like the behavior near the boundary is a bit smoother after fixing this even.
I had a bunch of tests that were slightly off because of this too.
| // A valid matrix is defined as a matrix where all sides of the viewport | ||
| // intersect the boundary. This allows the viewport to partially see beyond | ||
| // the boundary after rotation, but keeps the entire boundary visible. | ||
| Matrix4 _validateMatrix(Matrix4 matrix, Matrix4 defaultMatrix) { |
There was a problem hiding this comment.
Would it also achieve the same goal if we compute the bounding box of the viewport and compare it to _boundaryRect to determine the amount of translation we need? That sounds a lot easier than dealing with line segments.
There was a problem hiding this comment.
I tried that way first, but I ended up preferring the user experience this way when near the boundary. If I force the axis aligned bounding box of the viewport to fit inside of the boundary, then I won't be able to see the corners of the child. Also, I assume that many uses of InteractiveViewer will have a similarly sized boundary and viewport initially, like a photo viewer, and in that case it's nice to be able to rotate at the default size without being stopped by the boundary.
Maybe this is something that could be configured in the long term since the behavior is different for the two approaches.
There was a problem hiding this comment.
Sorry I probably don't fully understand how this works, but from
A valid matrix is defined as a matrix where all sides of the viewport intersect the boundary.
It sounds like a necessary and sufficient condition for _boundaryRect to intersect all 4 edges of nextViewport is:
_boundaryRect.topis in [nextViewport.smallestY,nextViewport.2ndSmallestY]_boundaryRect.bottmis in [nextViewport.2ndLargestY,nextViewport.largestY]- 4, similarly we can get the horizontal constraints for
_boundaryRect.leftand_boundaryRect.right.
_boundaryRect.bottm - _boundaryRect.top is a constant.
So the problem turns into finding the dx and dy that minimize dx^2 + dy^2 and satisfy all 4 constaints, or one of them in each direction if they can't be satisfied at the same time, i.e.:
_boundaryRect.top + dy in [nextViewport.smallestY, nextViewport.2ndSmallestY] and/or [_boundaryRect.height - nextViewport.largestY, _boundaryRect.height - nextViewport.2ndLargestY]
&&
_boundaryRect.left + dx in [nextViewport.smallestX, nextViewport.2ndSmallestX] and/or [_boundaryRect.width - nextViewport.largestX, _boundaryRect.width - nextViewport.2ndLargestX]
There was a problem hiding this comment.
I think you might be on to something, but there is a case where this doesn't work if I understand correctly.
Sorry for the bad drawing but imagine the pen rectangle is the boundary and the pencil rectangle is the viewport:
I would consider this to be invalid because one edge of the viewport does not intersect the boundary. However, _boundaryRect.top is in [nextViewport.smallestY, nextViewport.2ndSmallestY].
There was a problem hiding this comment.
There're 4 constraints (2 horizontal and 2 vertical) and to make a viewport valid it has to satisfy all 4. In the drawing the viewport only satisfies 2 (one on each axis): _boundaryRect.top is in [nextViewport.smallestY, nextViewport.2ndSmallestY] but _boundaryRect.bottm is not in [nextViewport.2ndLargestY, nextViewport.largestY] so this is indeed invalid.
There was a problem hiding this comment.
ohhh it's neither sufficient or necessary (counter example: https://jamboard.google.com/d/1l4Gg2AAs8d_8fGEAK_diDle-9qSQjYYFPkxpzUXAS84/viewer?f=0). Let me see if it can be improved. I think we can make more use of the fact that the boundary is a Rect to simplify the calculation.
There was a problem hiding this comment.
I think the edges of the viewport must fall in the red areas of https://jamboard.google.com/d/1l4Gg2AAs8d_8fGEAK_diDle-9qSQjYYFPkxpzUXAS84/viewer?f=1, but that's not necessary: https://jamboard.google.com/d/1l4Gg2AAs8d_8fGEAK_diDle-9qSQjYYFPkxpzUXAS84/viewer?f=2
|
@RomanJos Thanks for the thorough review! Your summary of the logic is correct. I've been able to fix some of your points:
I'll make another attempt at fixing the _gestureType problem soon. |
| final ClosestPoints closestPoints = invalidSide.findClosestPointsRect(_boundaryRect); | ||
| final Offset difference = closestPoints.a - closestPoints.b; | ||
| if (magnitudesAligned) { | ||
| final bool xMisaligned = difference.dx != 0.0 && value.dx != 0.0 |
There was a problem hiding this comment.
nit: equivalent to difference.dx * value.dx < 0 (i.e. having different signs)?
| ); | ||
|
|
||
| // If this matrix is invalid and uncorrectable, then return defaultMatrix. | ||
| if (!correctedViewportSide01.intersectsRect(_boundaryRect) |
There was a problem hiding this comment.
so for cases like this: https://jamboard.google.com/d/1l4Gg2AAs8d_8fGEAK_diDle-9qSQjYYFPkxpzUXAS84/viewer?f=1, this will try to correct the matrix by moving the child to the right edge of the viewport because it's farther, then return the default matrix because the top left corner of the child is inside the viewport?
|
@justinmc Do you still have plans for this one? |
|
Yes, but I'll close it for now until I have time to look at it again. |
|
Any chance of this being looked at again? I understand if you don't have the time! |
|
I have completed a similar project at https://github.com/UdaraWanasinghe/android-transform-layout. The issue with your implementation is that it handles translation, scale, and rotation separately based on the GestureType. But it's possible to do it simultaneously. |




Description
This PR adds support for rotation to InteractiveViewer:
Screen.Recording.2021-01-05.at.11.29.35.AM.mov
I decided that the most intuitive behavior was to prevent any full side of the viewport from viewing beyond the boundary. This creates an intuitive experience in most cases by allowing the user to see beyond the boundary (the pink in the video above), but not to fully move beyond it.
Related Issues
Closes #57698
Tests
I added the following tests:
Breaking Change
Shouldn't be a breaking change, though rotation will be enabled by default. Existing usages of InteractiveViewer that don't want rotation should explicitly set
rotateEnabledto false. If anyone thinks that this will negatively affect many users, I'm open to defaulting rotateEnabled to true, though it seems more logical to me that all of theenabledparameters default to true.