Commit 600c75cb authored by Adam Barth's avatar Adam Barth Committed by GitHub

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.
parent 9ff2e978
...@@ -364,6 +364,7 @@ class SliverGeometry { ...@@ -364,6 +364,7 @@ class SliverGeometry {
this.maxPaintExtent: 0.0, this.maxPaintExtent: 0.0,
double hitTestExtent, double hitTestExtent,
bool visible, bool visible,
this.hasVisualOverflow: false,
this.scrollOffsetCorrection: 0.0 this.scrollOffsetCorrection: 0.0
}) : layoutExtent = layoutExtent ?? paintExtent, }) : layoutExtent = layoutExtent ?? paintExtent,
hitTestExtent = hitTestExtent ?? paintExtent, hitTestExtent = hitTestExtent ?? paintExtent,
...@@ -416,6 +417,13 @@ class SliverGeometry { ...@@ -416,6 +417,13 @@ class SliverGeometry {
/// false if [paintExtent] is zero. /// false if [paintExtent] is zero.
final bool visible; 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 /// 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 /// offset will be adjusted by the parent and then the entire layout of the
/// parent will be rerun. /// parent will be rerun.
...@@ -451,6 +459,7 @@ class SliverGeometry { ...@@ -451,6 +459,7 @@ class SliverGeometry {
assert(hitTestExtent != null); assert(hitTestExtent != null);
assert(hitTestExtent >= 0.0); assert(hitTestExtent >= 0.0);
assert(visible != null); assert(visible != null);
assert(hasVisualOverflow != null);
assert(scrollOffsetCorrection != null); assert(scrollOffsetCorrection != null);
assert(scrollOffsetCorrection == 0.0); assert(scrollOffsetCorrection == 0.0);
return true; return true;
...@@ -481,6 +490,8 @@ class SliverGeometry { ...@@ -481,6 +490,8 @@ class SliverGeometry {
buffer.write('maxPaintExtent: ${maxPaintExtent.toStringAsFixed(1)}, '); buffer.write('maxPaintExtent: ${maxPaintExtent.toStringAsFixed(1)}, ');
if (hitTestExtent != paintExtent) if (hitTestExtent != paintExtent)
buffer.write('hitTestExtent: ${hitTestExtent.toStringAsFixed(1)}, '); buffer.write('hitTestExtent: ${hitTestExtent.toStringAsFixed(1)}, ');
if (hasVisualOverflow)
buffer.write('hasVisualOverflow: true, ');
buffer.write('scrollOffsetCorrection: ${scrollOffsetCorrection.toStringAsFixed(1)}'); buffer.write('scrollOffsetCorrection: ${scrollOffsetCorrection.toStringAsFixed(1)}');
buffer.write(')'); buffer.write(')');
return buffer.toString(); return buffer.toString();
...@@ -1301,6 +1312,7 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl ...@@ -1301,6 +1312,7 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
double _minScrollExtent; double _minScrollExtent;
double _maxScrollExtent; double _maxScrollExtent;
double _shrinkWrapExtent; double _shrinkWrapExtent;
bool _hasVisualOverflow = false;
@override @override
void performLayout() { void performLayout() {
...@@ -1321,6 +1333,7 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl ...@@ -1321,6 +1333,7 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
_minScrollExtent = 0.0; _minScrollExtent = 0.0;
_maxScrollExtent = 0.0; _maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0; _shrinkWrapExtent = 0.0;
_hasVisualOverflow = false;
offset.applyContentDimensions(0.0, 0.0); offset.applyContentDimensions(0.0, 0.0);
return; return;
} }
...@@ -1416,6 +1429,7 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl ...@@ -1416,6 +1429,7 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
_minScrollExtent = 0.0; _minScrollExtent = 0.0;
_maxScrollExtent = 0.0; _maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0; _shrinkWrapExtent = 0.0;
_hasVisualOverflow = false;
// centerOffset is the offset from the leading edge of the RenderViewport2 // centerOffset is the offset from the leading edge of the RenderViewport2
// to the zero scroll offset (the line between the forward slivers and the // to the zero scroll offset (the line between the forward slivers and the
...@@ -1526,6 +1540,9 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl ...@@ -1526,6 +1540,9 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
} }
_shrinkWrapExtent += childLayoutGeometry.maxPaintExtent; _shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
if (childLayoutGeometry.hasVisualOverflow)
_hasVisualOverflow = true;
// move on to the next child // move on to the next child
assert(child.parentData == childParentData); assert(child.parentData == childParentData);
child = advance(child); child = advance(child);
...@@ -1559,17 +1576,11 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl ...@@ -1559,17 +1576,11 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
childParentData.applyPaintTransform(transform); childParentData.applyPaintTransform(transform);
} }
@override void _paintContents(PaintingContext context, Offset offset) {
void paint(PaintingContext context, Offset offset) {
if (center == null) {
assert(firstChild == null);
return;
}
assert(center.parent == this); assert(center.parent == this);
assert(firstChild != null); assert(firstChild != null);
RenderSliver child;
// TODO(ianh): if we have content beyond our max extents, clip RenderSliver child = firstChild;
child = firstChild;
while (child != center) { while (child != center) {
if (child.geometry.visible) { if (child.geometry.visible) {
final SliverPhysicalParentData childParentData = child.parentData; final SliverPhysicalParentData childParentData = child.parentData;
...@@ -1589,6 +1600,20 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl ...@@ -1589,6 +1600,20 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
} }
} }
@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 @override
void debugPaintSize(PaintingContext context, Offset offset) { void debugPaintSize(PaintingContext context, Offset offset) {
assert(() { assert(() {
...@@ -1781,6 +1806,7 @@ class RenderSliverToBoxAdapter extends RenderSliver with RenderObjectWithChildMi ...@@ -1781,6 +1806,7 @@ class RenderSliverToBoxAdapter extends RenderSliver with RenderObjectWithChildMi
paintExtent: paintedChildSize, paintExtent: paintedChildSize,
maxPaintExtent: childExtent, maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize, hitTestExtent: paintedChildSize,
hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
); );
final SliverPhysicalParentData childParentData = child.parentData; final SliverPhysicalParentData childParentData = child.parentData;
......
...@@ -210,6 +210,7 @@ abstract class RenderSliverScrollingAppBar extends RenderSliverAppBar { ...@@ -210,6 +210,7 @@ abstract class RenderSliverScrollingAppBar extends RenderSliverAppBar {
scrollExtent: maxExtent, scrollExtent: maxExtent,
paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent), paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
maxPaintExtent: maxExtent, maxPaintExtent: maxExtent,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
); );
_childPosition = math.min(0.0, paintExtent - childExtent); _childPosition = math.min(0.0, paintExtent - childExtent);
} }
...@@ -240,6 +241,7 @@ abstract class RenderSliverPinnedAppBar extends RenderSliverAppBar { ...@@ -240,6 +241,7 @@ abstract class RenderSliverPinnedAppBar extends RenderSliverAppBar {
paintExtent: math.min(constraints.overlap + childExtent, constraints.remainingPaintExtent), paintExtent: math.min(constraints.overlap + childExtent, constraints.remainingPaintExtent),
layoutExtent: (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent), layoutExtent: (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent),
maxPaintExtent: constraints.overlap + maxExtent, maxPaintExtent: constraints.overlap + maxExtent,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
); );
} }
...@@ -289,6 +291,7 @@ abstract class RenderSliverFloatingAppBar extends RenderSliverAppBar { ...@@ -289,6 +291,7 @@ abstract class RenderSliverFloatingAppBar extends RenderSliverAppBar {
paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent), paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
layoutExtent: layoutExtent, layoutExtent: layoutExtent,
maxPaintExtent: maxExtent, maxPaintExtent: maxExtent,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
); );
_childPosition = math.min(0.0, paintExtent - childExtent); _childPosition = math.min(0.0, paintExtent - childExtent);
_lastActualScrollOffset = constraints.scrollOffset; _lastActualScrollOffset = constraints.scrollOffset;
......
...@@ -497,6 +497,8 @@ abstract class RenderSliverBlock extends RenderSliver ...@@ -497,6 +497,8 @@ abstract class RenderSliverBlock extends RenderSliver
scrollExtent: estimatedTotalExtent, scrollExtent: estimatedTotalExtent,
paintExtent: paintedExtent, paintExtent: paintedExtent,
maxPaintExtent: estimatedTotalExtent, maxPaintExtent: estimatedTotalExtent,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0,
); );
assert(_currentlyUpdatingChildIndex == null); assert(_currentlyUpdatingChildIndex == null);
......
...@@ -232,6 +232,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R ...@@ -232,6 +232,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
mainAxisPaddingPaintExtent + childLayoutGeometry.paintExtent, mainAxisPaddingPaintExtent + childLayoutGeometry.paintExtent,
beforePaddingPaintExtent + childLayoutGeometry.hitTestExtent, beforePaddingPaintExtent + childLayoutGeometry.hitTestExtent,
), ),
hasVisualOverflow: childLayoutGeometry.hasVisualOverflow,
); );
final SliverPhysicalParentData childParentData = child.parentData; final SliverPhysicalParentData childParentData = child.parentData;
......
...@@ -107,6 +107,18 @@ abstract class PaintPattern { ...@@ -107,6 +107,18 @@ abstract class PaintPattern {
/// See also: [save], [restore]. /// See also: [save], [restore].
void saveRestore(); 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. /// Indicates that a rectangle is expected next.
/// ///
/// The next rectangle is examined. Any arguments that are passed to this /// The next rectangle is examined. Any arguments that are passed to this
...@@ -227,6 +239,11 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern ...@@ -227,6 +239,11 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern
_predicates.add(new _SaveRestorePairPaintPredicate()); _predicates.add(new _SaveRestorePairPaintPredicate());
} }
@override
void clipRect({ Rect rect }) {
_predicates.add(new _FunctionPaintPredicate(#clipRect, <dynamic>[rect]));
}
@override @override
void rect({ Rect rect, Color color, bool hasMaskFilter, PaintingStyle style }) { void rect({ Rect rect, Color color, bool hasMaskFilter, PaintingStyle style }) {
_predicates.add(new _RectPaintPredicate(rect: rect, color: color, hasMaskFilter: hasMaskFilter, style: style)); _predicates.add(new _RectPaintPredicate(rect: rect, color: color, hasMaskFilter: hasMaskFilter, style: style));
...@@ -399,6 +416,14 @@ class _TestRecordingPaintingContext implements PaintingContext { ...@@ -399,6 +416,14 @@ class _TestRecordingPaintingContext implements PaintingContext {
child.paint(this, offset); child.paint(this, offset);
} }
@override
void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter) {
canvas.save();
canvas.clipRect(clipRect.shift(offset));
painter(this, offset);
canvas.restore();
}
@override @override
void noSuchMethod(Invocation invocation) { void noSuchMethod(Invocation invocation) {
} }
......
...@@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../rendering/mock_canvas.dart';
Future<Null> test(WidgetTester tester, double offset) { Future<Null> test(WidgetTester tester, double offset) {
return tester.pumpWidget(new Viewport2( return tester.pumpWidget(new Viewport2(
offset: new ViewportOffset.fixed(offset), offset: new ViewportOffset.fixed(offset),
...@@ -156,4 +158,104 @@ void main() { ...@@ -156,4 +158,104 @@ void main() {
const Point(0.0, 504.0), const Point(0.0, 504.0),
], 'acb'); ], '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());
});
} }
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