Viewport2 should clip visual overflow (#7641)
When you put a box in a Viewport2, it might paint outside its allocated bounds.
This patch teaches Viewport2 to clip when that happens.
diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart
index 15d7708..6db86cc 100644
--- a/packages/flutter/lib/src/rendering/sliver.dart
+++ b/packages/flutter/lib/src/rendering/sliver.dart
@@ -364,6 +364,7 @@
this.maxPaintExtent: 0.0,
double hitTestExtent,
bool visible,
+ this.hasVisualOverflow: false,
this.scrollOffsetCorrection: 0.0
}) : layoutExtent = layoutExtent ?? paintExtent,
hitTestExtent = hitTestExtent ?? paintExtent,
@@ -416,6 +417,13 @@
/// false if [paintExtent] is zero.
final bool visible;
+ /// Whether this sliver has visual overflow.
+ ///
+ /// By default, this is false, which means the viewport does not need to clip
+ /// its children. If any slivers have visual overflow, the viewport will apply
+ /// a clip to its children.
+ final bool hasVisualOverflow;
+
/// If this is non-zero after [RenderSliver.performLayout] returns, the scroll
/// offset will be adjusted by the parent and then the entire layout of the
/// parent will be rerun.
@@ -451,6 +459,7 @@
assert(hitTestExtent != null);
assert(hitTestExtent >= 0.0);
assert(visible != null);
+ assert(hasVisualOverflow != null);
assert(scrollOffsetCorrection != null);
assert(scrollOffsetCorrection == 0.0);
return true;
@@ -481,6 +490,8 @@
buffer.write('maxPaintExtent: ${maxPaintExtent.toStringAsFixed(1)}, ');
if (hitTestExtent != paintExtent)
buffer.write('hitTestExtent: ${hitTestExtent.toStringAsFixed(1)}, ');
+ if (hasVisualOverflow)
+ buffer.write('hasVisualOverflow: true, ');
buffer.write('scrollOffsetCorrection: ${scrollOffsetCorrection.toStringAsFixed(1)}');
buffer.write(')');
return buffer.toString();
@@ -1301,6 +1312,7 @@
double _minScrollExtent;
double _maxScrollExtent;
double _shrinkWrapExtent;
+ bool _hasVisualOverflow = false;
@override
void performLayout() {
@@ -1321,6 +1333,7 @@
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0;
+ _hasVisualOverflow = false;
offset.applyContentDimensions(0.0, 0.0);
return;
}
@@ -1416,6 +1429,7 @@
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0;
+ _hasVisualOverflow = false;
// centerOffset is the offset from the leading edge of the RenderViewport2
// to the zero scroll offset (the line between the forward slivers and the
@@ -1526,6 +1540,9 @@
}
_shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
+ if (childLayoutGeometry.hasVisualOverflow)
+ _hasVisualOverflow = true;
+
// move on to the next child
assert(child.parentData == childParentData);
child = advance(child);
@@ -1559,17 +1576,11 @@
childParentData.applyPaintTransform(transform);
}
- @override
- void paint(PaintingContext context, Offset offset) {
- if (center == null) {
- assert(firstChild == null);
- return;
- }
+ void _paintContents(PaintingContext context, Offset offset) {
assert(center.parent == this);
assert(firstChild != null);
- RenderSliver child;
- // TODO(ianh): if we have content beyond our max extents, clip
- child = firstChild;
+
+ RenderSliver child = firstChild;
while (child != center) {
if (child.geometry.visible) {
final SliverPhysicalParentData childParentData = child.parentData;
@@ -1590,6 +1601,20 @@
}
@override
+ void paint(PaintingContext context, Offset offset) {
+ if (center == null) {
+ assert(firstChild == null);
+ return;
+ }
+
+ if (_hasVisualOverflow) {
+ context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
+ } else {
+ _paintContents(context, offset);
+ }
+ }
+
+ @override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
super.debugPaintSize(context, offset);
@@ -1781,6 +1806,7 @@
paintExtent: paintedChildSize,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
+ hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
final SliverPhysicalParentData childParentData = child.parentData;
diff --git a/packages/flutter/lib/src/rendering/sliver_app_bar.dart b/packages/flutter/lib/src/rendering/sliver_app_bar.dart
index d8efac1..b2230fe 100644
--- a/packages/flutter/lib/src/rendering/sliver_app_bar.dart
+++ b/packages/flutter/lib/src/rendering/sliver_app_bar.dart
@@ -210,6 +210,7 @@
scrollExtent: maxExtent,
paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
maxPaintExtent: maxExtent,
+ hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
_childPosition = math.min(0.0, paintExtent - childExtent);
}
@@ -240,6 +241,7 @@
paintExtent: math.min(constraints.overlap + childExtent, constraints.remainingPaintExtent),
layoutExtent: (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent),
maxPaintExtent: constraints.overlap + maxExtent,
+ hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
}
@@ -289,6 +291,7 @@
paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
layoutExtent: layoutExtent,
maxPaintExtent: maxExtent,
+ hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
_childPosition = math.min(0.0, paintExtent - childExtent);
_lastActualScrollOffset = constraints.scrollOffset;
diff --git a/packages/flutter/lib/src/rendering/sliver_block.dart b/packages/flutter/lib/src/rendering/sliver_block.dart
index e89fbda..cf6de77 100644
--- a/packages/flutter/lib/src/rendering/sliver_block.dart
+++ b/packages/flutter/lib/src/rendering/sliver_block.dart
@@ -497,6 +497,8 @@
scrollExtent: estimatedTotalExtent,
paintExtent: paintedExtent,
maxPaintExtent: estimatedTotalExtent,
+ // Conservative to avoid flickering away the clip during scroll.
+ hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0,
);
assert(_currentlyUpdatingChildIndex == null);
diff --git a/packages/flutter/lib/src/rendering/sliver_padding.dart b/packages/flutter/lib/src/rendering/sliver_padding.dart
index a85b1d6..6d3731a 100644
--- a/packages/flutter/lib/src/rendering/sliver_padding.dart
+++ b/packages/flutter/lib/src/rendering/sliver_padding.dart
@@ -232,6 +232,7 @@
mainAxisPaddingPaintExtent + childLayoutGeometry.paintExtent,
beforePaddingPaintExtent + childLayoutGeometry.hitTestExtent,
),
+ hasVisualOverflow: childLayoutGeometry.hasVisualOverflow,
);
final SliverPhysicalParentData childParentData = child.parentData;
diff --git a/packages/flutter/test/rendering/mock_canvas.dart b/packages/flutter/test/rendering/mock_canvas.dart
index f3c512c..6919bc0 100644
--- a/packages/flutter/test/rendering/mock_canvas.dart
+++ b/packages/flutter/test/rendering/mock_canvas.dart
@@ -107,6 +107,18 @@
/// See also: [save], [restore].
void saveRestore();
+ /// Indicates that a rectangular clip.
+ ///
+ /// The next rectangular clip is examined. Any arguments that are passed to
+ /// this method are compared to the actual [Canvas.clipRect] call's argument
+ /// and any mismatches result in failure.
+ ///
+ /// If no call to [Canvas.clipRect] was made, then this results in failure.
+ ///
+ /// Any calls made between the last matched call (if any) and the
+ /// [Canvas.clipRect] call are ignored.
+ void clipRect({ Rect rect });
+
/// Indicates that a rectangle is expected next.
///
/// The next rectangle is examined. Any arguments that are passed to this
@@ -228,6 +240,11 @@
}
@override
+ void clipRect({ Rect rect }) {
+ _predicates.add(new _FunctionPaintPredicate(#clipRect, <dynamic>[rect]));
+ }
+
+ @override
void rect({ Rect rect, Color color, bool hasMaskFilter, PaintingStyle style }) {
_predicates.add(new _RectPaintPredicate(rect: rect, color: color, hasMaskFilter: hasMaskFilter, style: style));
}
@@ -400,6 +417,14 @@
}
@override
+ void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter) {
+ canvas.save();
+ canvas.clipRect(clipRect.shift(offset));
+ painter(this, offset);
+ canvas.restore();
+ }
+
+ @override
void noSuchMethod(Invocation invocation) {
}
}
diff --git a/packages/flutter/test/widgets/slivers_block_test.dart b/packages/flutter/test/widgets/slivers_block_test.dart
index a379249..40cf371 100644
--- a/packages/flutter/test/widgets/slivers_block_test.dart
+++ b/packages/flutter/test/widgets/slivers_block_test.dart
@@ -6,6 +6,8 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
+import '../rendering/mock_canvas.dart';
+
Future<Null> test(WidgetTester tester, double offset) {
return tester.pumpWidget(new Viewport2(
offset: new ViewportOffset.fixed(offset),
@@ -156,4 +158,104 @@
const Point(0.0, 504.0),
], 'acb');
});
+
+ testWidgets('Viewport2 overflow clipping of SliverToBoxAdapter', (WidgetTester tester) async {
+ await tester.pumpWidget(new Viewport2(
+ offset: new ViewportOffset.zero(),
+ children: <Widget>[
+ new SliverToBoxAdapter(
+ child: new SizedBox(height: 400.0, child: new Text('a')),
+ ),
+ ],
+ ));
+
+ expect(find.byType(Viewport2), isNot(paints..clipRect()));
+
+ await tester.pumpWidget(new Viewport2(
+ offset: new ViewportOffset.fixed(100.0),
+ children: <Widget>[
+ new SliverToBoxAdapter(
+ child: new SizedBox(height: 400.0, child: new Text('a')),
+ ),
+ ],
+ ));
+
+ expect(find.byType(Viewport2), paints..clipRect());
+
+ await tester.pumpWidget(new Viewport2(
+ offset: new ViewportOffset.fixed(100.0),
+ children: <Widget>[
+ new SliverToBoxAdapter(
+ child: new SizedBox(height: 4000.0, child: new Text('a')),
+ ),
+ ],
+ ));
+
+ expect(find.byType(Viewport2), paints..clipRect());
+
+ await tester.pumpWidget(new Viewport2(
+ offset: new ViewportOffset.zero(),
+ children: <Widget>[
+ new SliverToBoxAdapter(
+ child: new SizedBox(height: 4000.0, child: new Text('a')),
+ ),
+ ],
+ ));
+
+ expect(find.byType(Viewport2), paints..clipRect());
+ });
+
+ testWidgets('Viewport2 overflow clipping of SliverBlock', (WidgetTester tester) async {
+ await tester.pumpWidget(new Viewport2(
+ offset: new ViewportOffset.zero(),
+ children: <Widget>[
+ new SliverBlock(
+ delegate: new SliverBlockChildListDelegate(<Widget>[
+ new SizedBox(height: 400.0, child: new Text('a')),
+ ]),
+ ),
+ ],
+ ));
+
+ expect(find.byType(Viewport2), isNot(paints..clipRect()));
+
+ await tester.pumpWidget(new Viewport2(
+ offset: new ViewportOffset.fixed(100.0),
+ children: <Widget>[
+ new SliverBlock(
+ delegate: new SliverBlockChildListDelegate(<Widget>[
+ new SizedBox(height: 400.0, child: new Text('a')),
+ ]),
+ ),
+ ],
+ ));
+
+ expect(find.byType(Viewport2), paints..clipRect());
+
+ await tester.pumpWidget(new Viewport2(
+ offset: new ViewportOffset.fixed(100.0),
+ children: <Widget>[
+ new SliverBlock(
+ delegate: new SliverBlockChildListDelegate(<Widget>[
+ new SizedBox(height: 4000.0, child: new Text('a')),
+ ]),
+ ),
+ ],
+ ));
+
+ expect(find.byType(Viewport2), paints..clipRect());
+
+ await tester.pumpWidget(new Viewport2(
+ offset: new ViewportOffset.zero(),
+ children: <Widget>[
+ new SliverBlock(
+ delegate: new SliverBlockChildListDelegate(<Widget>[
+ new SizedBox(height: 4000.0, child: new Text('a')),
+ ]),
+ ),
+ ],
+ ));
+
+ expect(find.byType(Viewport2), paints..clipRect());
+ });
}