Unverified Commit 56039b4b authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Make getOffsetToReveal work with nested Viewports (#26663)

Fixes #20893, the most-upvoted bug for framework.

A detailed analysis explaining the cause of the bug has been posted at #20893 (comment).
parent 7e22b5f2
...@@ -57,9 +57,9 @@ abstract class RenderAbstractViewport extends RenderObject { ...@@ -57,9 +57,9 @@ abstract class RenderAbstractViewport extends RenderObject {
/// edge of the viewport as possible. If `alignment` is 0.5, the child must be /// edge of the viewport as possible. If `alignment` is 0.5, the child must be
/// positioned as close to the center of the viewport as possible. /// positioned as close to the center of the viewport as possible.
/// ///
/// The target might not be a direct child of this viewport but it must be a /// The `target` might not be a direct child of this viewport but it must be a
/// descendant of the viewport and there must not be any other /// descendant of the viewport. Other viewports in between this viewport and
/// [RenderAbstractViewport] objects between the target and this object. /// the `target` will not be adjusted.
/// ///
/// This method assumes that the content of the viewport moves linearly, i.e. /// This method assumes that the content of the viewport moves linearly, i.e.
/// when the offset of the viewport is changed by x then `target` also moves /// when the offset of the viewport is changed by x then `target` also moves
...@@ -585,26 +585,39 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -585,26 +585,39 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
@override @override
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) { RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
double leadingScrollOffset; double leadingScrollOffset = 0.0;
double targetMainAxisExtent; double targetMainAxisExtent;
RenderObject descendant;
rect ??= target.paintBounds; rect ??= target.paintBounds;
if (target is RenderBox) { // Starting at `target` and walking towards the root:
final RenderBox targetBox = target; // - `child` will be the last object before we reach this viewport, and
// - `pivot` will be the last RenderBox before we reach this viewport.
// The pivot will be the topmost child before we hit a RenderSliver. RenderObject child = target;
RenderBox pivot = targetBox; RenderBox pivot;
while (pivot.parent is RenderBox) bool onlySlivers = target is RenderSliver; // ... between viewport and `target` (`target` included).
pivot = pivot.parent; while (child.parent != this) {
assert(child.parent != null, '$target must be a descendant of $this');
if (child is RenderBox) {
pivot = child;
}
if (child.parent is RenderSliver) {
final RenderSliver parent = child.parent;
leadingScrollOffset += parent.childScrollOffset(child);
} else {
onlySlivers = false;
leadingScrollOffset = 0.0;
}
child = child.parent;
}
if (pivot != null) {
assert(pivot.parent != null); assert(pivot.parent != null);
assert(pivot.parent != this); assert(pivot.parent != this);
assert(pivot != this); assert(pivot != this);
assert(pivot.parent is RenderSliver); // TODO(abarth): Support other kinds of render objects besides slivers. assert(pivot.parent is RenderSliver); // TODO(abarth): Support other kinds of render objects besides slivers.
final RenderSliver pivotParent = pivot.parent; final RenderSliver pivotParent = pivot.parent;
final Matrix4 transform = targetBox.getTransformTo(pivot); final Matrix4 transform = target.getTransformTo(pivot);
final Rect bounds = MatrixUtils.transformRect(transform, rect); final Rect bounds = MatrixUtils.transformRect(transform, rect);
final GrowthDirection growthDirection = pivotParent.constraints.growthDirection; final GrowthDirection growthDirection = pivotParent.constraints.growthDirection;
...@@ -619,15 +632,15 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -619,15 +632,15 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
offset = bounds.top; offset = bounds.top;
break; break;
} }
leadingScrollOffset = pivot.size.height - offset; leadingScrollOffset += pivot.size.height - offset;
targetMainAxisExtent = bounds.height; targetMainAxisExtent = bounds.height;
break; break;
case AxisDirection.right: case AxisDirection.right:
leadingScrollOffset = bounds.left; leadingScrollOffset += bounds.left;
targetMainAxisExtent = bounds.width; targetMainAxisExtent = bounds.width;
break; break;
case AxisDirection.down: case AxisDirection.down:
leadingScrollOffset = bounds.top; leadingScrollOffset += bounds.top;
targetMainAxisExtent = bounds.height; targetMainAxisExtent = bounds.height;
break; break;
case AxisDirection.left: case AxisDirection.left:
...@@ -640,28 +653,17 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -640,28 +653,17 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
offset = bounds.left; offset = bounds.left;
break; break;
} }
leadingScrollOffset = pivot.size.width - offset; leadingScrollOffset += pivot.size.width - offset;
targetMainAxisExtent = bounds.width; targetMainAxisExtent = bounds.width;
break; break;
} }
descendant = pivot; } else if (onlySlivers) {
} else if (target is RenderSliver) {
final RenderSliver targetSliver = target; final RenderSliver targetSliver = target;
leadingScrollOffset = 0.0;
targetMainAxisExtent = targetSliver.geometry.scrollExtent; targetMainAxisExtent = targetSliver.geometry.scrollExtent;
descendant = targetSliver;
} else { } else {
return RevealedOffset(offset: offset.pixels, rect: rect); return RevealedOffset(offset: offset.pixels, rect: rect);
} }
// The child will be the topmost object before we get to the viewport.
RenderObject child = descendant;
while (child.parent is RenderSliver) {
final RenderSliver parent = child.parent;
leadingScrollOffset += parent.childScrollOffset(child);
child = parent;
}
assert(child.parent == this); assert(child.parent == this);
assert(child is RenderSliver); assert(child is RenderSliver);
final RenderSliver sliver = child; final RenderSliver sliver = child;
......
...@@ -642,6 +642,118 @@ void main() { ...@@ -642,6 +642,118 @@ void main() {
}); });
}); });
testWidgets('Nested Viewports showOnScreen with allowImplicitScrolling=false for inner viewport', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/20893.
List<Widget> slivers;
final ScrollController controllerX = ScrollController(initialScrollOffset: 0.0);
final ScrollController controllerY = ScrollController(initialScrollOffset: 0.0);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Container(
height: 200.0,
width: 200.0,
child: ListView(
controller: controllerY,
children: <Widget>[
Container(
height: 150.0,
),
Container(
height: 100.0,
child: ListView(
physics: const PageScrollPhysics(), // Turns off `allowImplicitScrolling`
scrollDirection: Axis.horizontal,
controller: controllerX,
children: slivers = <Widget>[
Container(
width: 150.0,
),
Container(
width: 150.0,
),
],
),
),
Container(
height: 150.0,
),
],
),
),
),
),
);
tester.renderObject(find.byWidget(slivers[1])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 0.0);
expect(controllerY.offset, 50.0);
});
testWidgets('Nested Viewports showOnScreen on Sliver with allowImplicitScrolling=false for inner viewport', (WidgetTester tester) async {
Widget sliver;
final ScrollController controllerX = ScrollController(initialScrollOffset: 0.0);
final ScrollController controllerY = ScrollController(initialScrollOffset: 0.0);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Container(
height: 200.0,
width: 200.0,
child: ListView(
controller: controllerY,
children: <Widget>[
Container(
height: 150.0,
),
Container(
height: 100.0,
child: CustomScrollView(
physics: const PageScrollPhysics(), // Turns off `allowImplicitScrolling`
scrollDirection: Axis.horizontal,
controller: controllerX,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(25.0),
sliver: SliverToBoxAdapter(
child: Container(
width: 100.0,
),
),
),
SliverPadding(
padding: const EdgeInsets.all(25.0),
sliver: sliver = SliverToBoxAdapter(
child: Container(
width: 100.0,
),
),
),
],
),
),
Container(
height: 150.0,
),
],
),
),
),
),
);
tester.renderObject(find.byWidget(sliver)).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 0.0);
expect(controllerY.offset, 25.0);
});
testWidgets('Viewport showOnScreen with objects larger than viewport', (WidgetTester tester) async { testWidgets('Viewport showOnScreen with objects larger than viewport', (WidgetTester tester) async {
List<Widget> children; List<Widget> children;
ScrollController controller; ScrollController controller;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment