fix(crop-rotate): stop crop handles, pinch and tilt from jumping - #855
Merged
Conversation
The crop handles were positioned absolutely on the pointer, so the distance the gesture recognizer swallows as slop (kPanSlop, 36px) plus the distance between the finger and the handle was applied as one jump before the handle started following the finger. Both are now captured at gesture start and compensated. The same applied to pinch-to-zoom, where the scale slop was applied as an instant zoom step. Further fixes in the same interaction: - Corner drags with a fixed aspect ratio derived the height from the width only, so a vertical drag did nothing and the corner drifted away from the finger. The pointer is now projected onto the ratio diagonal and the result is clamped to the image. - ScaleGestureRecognizer reports an end whenever the pointer count changes. That ran the full crop teardown when the second finger touched down and blocked the following start, leaving the pinch with a stale scale baseline. - The zoom-out hit area compared a raw screen position against the editor body size, which is inset on Android (maxWidthFactor) and sits below the app bar. Pointer positions are converted into the body's coordinate space instead of hand-rolling the offsets. - The overlay outside the crop area faded with two competing loops that each restarted from 0, so interrupting one snapped the brightness. It is driven by an AnimationController now. - While tilted, the bounds math overrode the zoom of the crop-end animation on every frame and the auto zoom-out fought the minimum zoom the tilt requires, which made the image jump around after a resize.
- Stop the auto zoom-out when the recognizer reports a mid-gesture end, so it no longer fights the pinch that follows. - Only latch the pinch baseline on a usable span; a scale of `0` used to disable the normalization for the whole gesture. - Release `_blockInteraction` when a degenerate crop rect aborts the scale end, which previously froze the editor for good. - Return a floor instead of `maxScale` from `_minCoveringScale` and keep resizing when the zoom-out can't run. - Skip the zoom-out instead of comparing a raw global position when the editor body has no render object. - Dispose the overlay `CurvedAnimation` and refresh its duration on every transition. - Hoist the fixed-ratio corner branch above the free-form clamping, pass the dragged part into both ratio helpers and compute the pointer hit point once per update.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Resizing the crop area felt broken: grabbing a corner and moving slowly produced an instant jump before the handle started following the finger. Pinch-to-zoom had the same jump, the darkening outside the crop area snapped instead of fading, and once a tilt was applied the image jumped around after every resize.
Cause
The crop handles were positioned absolutely on the pointer.
ScaleGestureRecognizeronly reports a drag after the pointer moved pastkPanSlop(36 logical px), and the handle hit area iskMinInteractiveDimension(48 px) wide — both distances were applied at once the moment the gesture was recognized. Pinch had the equivalent problem with the scale slop.On top of that, five independent defects in the same interaction:
bottom = top + width * ratio), so a vertical drag did nothing and diagonal drags drifted away from the finger. The result was also never clamped, so the selection could leave the image.onScaleEndfires on every pointer-count change, not just at the end of a gesture. The second finger of a pinch therefore ran the full crop teardown (animating the selection back and blocking the followingonScaleStart), leaving the pinch with a stale scale baseline.editorBodySize.width, which on Android is only 90 % of the screen and centered (maxWidthFactor). That created a dead band along the right edge where drag updates were silently dropped. The vertical test hand-rolled- kToolbarHeight - MediaQuery.padding.top, which breaks with custom or absent app bars.loopWithTransitionTimingloops that each restarted from0/1, so interrupting one snapped the brightness._setOffsetLimitsoverrode the zoom of the crop-end animation on every frame (itsmanualScaleFactorfloor was stale), and_zoomOutsidelooped against the minimum zoom the tilt requires whilecalcCropRect()reset the selection every 16 ms.Changes
_cropGrabOffset_pinchScaleBaseline_resizeCornerToRatio_resizeEdgeToRatio_toEditorBodyPositionglobalToLocal._interactionOpacityCtrlAnimationController+CurvedAnimation(same curve both directions, so reversing midway stays continuous)._onScaleEnddetails.pointerCount > 0)._isTilted/_minCoveringScalemanualScaleFactorin sync so the tilt bounds may only lift the zoom, and_zoomOutsidefloors at the zoom the tilt requires._hasToolbarand_editorScreenOffsetHelperare gone with the hand-rolled offset math;_editorContentKeymoved from the outerSafeAreato the editor body.Tests
Three new cases in
CropRotateEditor corner drag:follows the pointer without jumping on the first move— grabs the handle 12 px inside the corner and asserts the selection only shrinks by the distance the pointer traveled, that a purely vertical move resizes too, and that the anchored corner never moves.keeps the crop rect inside the image— dragging far outside must not push the selection past the view rect.resizes without losing the tilt zoom— the selection stays covered during the drag and is zoomed up to the correct target afterwards, with the tilt untouched.Full suite: 532 passing, analyzer clean.
The frame-by-frame smoothness of the crop-end animation is not asserted directly —
loopWithTransitionTimingdrives itself off wall-clockDateTime.now(), which degenerates underflutter_test's fake clock. The tests lock in the end state instead.Note
Compensating the slop means the handle now permanently trails the finger by however far it moved before the gesture was recognized. That is the trade-off for removing the jump.