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
Prevent viewport.showOnScreen from scrolling the viewport if the specified Rect is already visible. #56413
Prevent viewport.showOnScreen from scrolling the viewport if the specified Rect is already visible. #56413
Changes from 4 commits
652577a
749f9d9
bc33a39
197ac2a
ce6b502
cf2e39a
666727c
0174eca
75b7c3d
5732292
0955d3a
34b397c
cad2f5e
83e97c9
2f3713d
563c72e
371ee55
132c82c
5f394ca
cfdb749
929b666
5cff319
3e27f05
29c5d7d
bc5d5c3
c79ebfa
b40787e
9772b0a
722493f
930a944
075d5d9
fcdaac7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -196,6 +196,68 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje | |
_lastStretchOffset = stretchOffset; | ||
} | ||
|
||
@override | ||
void showOnScreen({ | ||
RenderObject descendant, | ||
Rect rect, | ||
Duration duration = Duration.zero, | ||
Curve curve = Curves.ease, | ||
}) { | ||
assert(child != null); | ||
final Rect childBounds = descendant != null | ||
? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds) | ||
: null; | ||
|
||
// Trims the part of `rect` that protrudes `child`'s leading edge. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does it need to stay in the bounds of the child? Doesn't it just have to stay within my own bounds? If that is the case, could this all be simplified to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right we only need to override |
||
// | ||
// If the `rect` a descendant specified in its showOnScreen call exceeds | ||
// the leading edge of this sliver (which is usually the same as that of | ||
// `child`), the viewport will move towards the leading edge (reduce its | ||
// scroll offset) to unpin the persistent header. This is almost always | ||
// undesirable. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It took me awhile to understand this comment, so I tried to clarify a bit. Definitely double check this and make sure I understood correctly, though.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 I'll put the comment above the Is it OK to limit the size of the caret's scroll padding instead, so that it will never exceed the leading edge (see the comment in "editable_text.dart" file)? That makes a little bit more sense to me (in case the caller of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense, but would it be a breaking change in normal situations? |
||
// | ||
// See: https://github.com/flutter/flutter/issues/25507. | ||
Rect trim(Rect original, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: instead of defining this inline, this code just be a private method on this object to declutter this method, no? |
||
double top = -double.infinity, | ||
double right = double.infinity, | ||
double bottom = double.infinity, | ||
double left = -double.infinity, | ||
}) { | ||
if (original == null) | ||
return null; | ||
|
||
return Rect.fromLTRB( | ||
math.max(original.left, left), | ||
math.max(original.top, top), | ||
math.min(original.right, right), | ||
math.min(original.bottom, bottom), | ||
); | ||
} | ||
|
||
Rect newRect; | ||
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { | ||
case AxisDirection.up: | ||
newRect = trim(childBounds ?? rect, bottom: childExtent); | ||
break; | ||
case AxisDirection.right: | ||
newRect = trim(childBounds ?? rect, left: 0); | ||
break; | ||
case AxisDirection.down: | ||
newRect = trim(childBounds ?? rect, top: 0); | ||
break; | ||
case AxisDirection.left: | ||
newRect = trim(childBounds ?? rect, right: childExtent); | ||
break; | ||
} | ||
|
||
super.showOnScreen( | ||
descendant: descendant == null ? this : child, | ||
rect: newRect, | ||
duration: duration, | ||
curve: curve, | ||
); | ||
} | ||
|
||
/// Returns the distance from the leading _visible_ edge of the sliver to the | ||
/// side of the child closest to that edge, in the scroll axis direction. | ||
/// | ||
|
@@ -585,6 +647,61 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste | |
_lastActualScrollOffset = constraints.scrollOffset; | ||
} | ||
|
||
@override | ||
void showOnScreen({ | ||
RenderObject descendant, | ||
Rect rect, | ||
Duration duration = Duration.zero, | ||
Curve curve = Curves.ease, | ||
}) { | ||
assert(child != null); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same child comment as above. |
||
final Rect childBounds = descendant != null | ||
? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds) | ||
: null; | ||
|
||
double minTargetExtent; | ||
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { | ||
case AxisDirection.up: | ||
minTargetExtent = childExtent - (childBounds?.top ?? 0); | ||
break; | ||
case AxisDirection.right: | ||
minTargetExtent = childBounds?.right ?? childExtent; | ||
break; | ||
case AxisDirection.down: | ||
minTargetExtent = childBounds?.bottom ?? childExtent; | ||
break; | ||
case AxisDirection.left: | ||
minTargetExtent = childExtent - (childBounds?.left ?? 0); | ||
break; | ||
} | ||
|
||
minTargetExtent = minTargetExtent.clamp(childExtent, maxExtent) as double; | ||
// Expands the header if needed, with animation if possible. | ||
if (minTargetExtent > childExtent) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I missed it, but did you have tests for when the header animates and for when it doesn't? |
||
if (snapConfiguration != null) { | ||
_controller ??= AnimationController(vsync: snapConfiguration.vsync, duration: duration); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. who starts this AnimationController? The only place I see calling "forward" on this sets a new animation to _animation overwriting the one set below? |
||
_controller.duration = duration; | ||
_animation = _controller | ||
.drive( | ||
Tween<double>( | ||
begin: _effectiveScrollOffset, | ||
end: maxExtent - minTargetExtent , | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't the idea of a snapping header that it will always snap to its max extend? |
||
).chain(CurveTween(curve: curve)), | ||
); | ||
} else { | ||
_effectiveScrollOffset = maxExtent - minTargetExtent; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the app bar just jumps from one size to the next in one frame? That seems ugly. |
||
markNeedsLayout(); | ||
} | ||
} | ||
|
||
super.showOnScreen( | ||
descendant: descendant == null ? this : child, | ||
rect: childBounds, | ||
duration: duration, | ||
curve: curve, | ||
); | ||
} | ||
|
||
@override | ||
double childMainAxisPosition(RenderBox child) { | ||
assert(child == this.child); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -783,7 +783,6 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix | |
final double offsetDifference = offset.pixels - targetOffset; | ||
|
||
final Matrix4 transform = target.getTransformTo(this); | ||
applyPaintTransform(child, transform); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems the paint transform that does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch. Did you add a test for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed. Adding this in a different PR. |
||
Rect targetRect = MatrixUtils.transformRect(transform, rect); | ||
|
||
switch (axisDirection) { | ||
|
@@ -986,16 +985,61 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix | |
); | ||
} | ||
|
||
final Rect newRect = RenderViewportBase.showInViewport( | ||
descendant: descendant, | ||
viewport: this, | ||
offset: offset, | ||
rect: rect, | ||
duration: duration, | ||
curve: curve, | ||
RenderObject child = descendant; | ||
RenderSliver childSliver; | ||
|
||
do { | ||
if (child is RenderSliver) { | ||
childSliver = child; | ||
} | ||
|
||
final RenderObject parent = child.parent as RenderObject; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if descendent (and thus child) is null? |
||
child = parent; | ||
assert(child != null, '$descendant must be a descendant of $this.'); | ||
} while (child != this); | ||
|
||
assert(childSliver.parent == this); | ||
Rect localRect = MatrixUtils.transformRect( | ||
(descendant ?? this).getTransformTo(this), | ||
rect ?? descendant?.paintBounds ?? paintBounds, | ||
); | ||
|
||
final double extentOfPinnedSlivers = maxScrollObstructionExtentBefore(childSliver); | ||
assert(extentOfPinnedSlivers >= 0); | ||
|
||
bool canSkipScrolling = true; | ||
switch (applyGrowthDirectionToAxisDirection(axisDirection, childSliver.constraints.growthDirection)) { | ||
case AxisDirection.up: | ||
canSkipScrolling = canSkipScrolling && localRect.top >= 0; | ||
canSkipScrolling = canSkipScrolling && localRect.bottom <= size.height - extentOfPinnedSlivers; | ||
break; | ||
case AxisDirection.right: | ||
canSkipScrolling = canSkipScrolling && localRect.left >= extentOfPinnedSlivers; | ||
canSkipScrolling = canSkipScrolling && localRect.right <= size.width; | ||
break; | ||
case AxisDirection.down: | ||
canSkipScrolling = canSkipScrolling && localRect.top >= extentOfPinnedSlivers; | ||
canSkipScrolling = canSkipScrolling && localRect.bottom <= size.height; | ||
break; | ||
case AxisDirection.left: | ||
canSkipScrolling = canSkipScrolling && localRect.left >= 0; | ||
canSkipScrolling = canSkipScrolling && localRect.right <= size.width - extentOfPinnedSlivers; | ||
break; | ||
} | ||
|
||
if (!canSkipScrolling) { | ||
localRect = RenderViewportBase.showInViewport( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. showInViewport already skips scrolling if the descendant is fully in view. What case does the logic above cover? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The added logic does assume the layout is up to date with the current There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @goderbauer got rid of the visibility check because the viewport could be scrolling / about to scroll. Could you take a look? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry nvm that broke some tests. Fixing. |
||
descendant: descendant, | ||
viewport: this, | ||
offset: offset, | ||
rect: rect, | ||
duration: duration, | ||
curve: curve, | ||
); | ||
} | ||
|
||
super.showOnScreen( | ||
rect: newRect, | ||
rect: localRect, | ||
duration: duration, | ||
curve: curve, | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1686,13 +1686,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien | |
bottomSpacing, | ||
); | ||
} | ||
final Rect inflatedRect = Rect.fromLTRB( | ||
newCaretRect.left - widget.scrollPadding.left, | ||
newCaretRect.top - widget.scrollPadding.top, | ||
newCaretRect.right + widget.scrollPadding.right, | ||
newCaretRect.bottom + bottomSpacing, | ||
); | ||
_editableKey.currentContext.findRenderObject().showOnScreen( | ||
|
||
final Rect inflatedRect = widget.scrollPadding | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative to the trimming in |
||
.copyWith(bottom: bottomSpacing) | ||
.inflateRect(newCaretRect); | ||
renderEditable.showOnScreen( | ||
rect: inflatedRect, | ||
duration: _caretAnimationDuration, | ||
curve: _caretAnimationCurve, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
child must only be != null if a descendent was provided, right? You could call showOnScreen on a child-less header to bring the entire header back on screen.