Unverified Commit 5c258366 authored by Dan Field's avatar Dan Field Committed by GitHub

Reland composite physical layers for all platforms (#29701)

* Composite physical layers even if elevation is 0
parent f1134358
b0ae19c7cbc01ca4ec24675b9f5b19e8ff44bd76
894e9634cda6eb940d1ad4efb20b63a174bb7797
......@@ -1580,12 +1580,8 @@ abstract class _RenderPhysicalModelBase<T> extends _RenderCustomClip<T> {
markNeedsPaint();
}
static final Paint _transparentPaint = Paint()..color = const Color(0x00000000);
// On Fuchsia, the system compositor is responsible for drawing shadows
// for physical model layers with non-zero elevation.
@override
bool get alwaysNeedsCompositing => _elevation != 0.0 && defaultTargetPlatform == TargetPlatform.fuchsia;
bool get alwaysNeedsCompositing => true;
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
......@@ -1715,37 +1711,14 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> {
}
return true;
}());
if (needsCompositing) {
final PhysicalModelLayer physicalModel = PhysicalModelLayer(
clipPath: offsetRRectAsPath,
clipBehavior: clipBehavior,
elevation: paintShadows ? elevation : 0.0,
color: color,
shadowColor: shadowColor,
);
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds);
} else {
final Canvas canvas = context.canvas;
if (elevation != 0.0 && paintShadows) {
// The drawShadow call doesn't add the region of the shadow to the
// picture's bounds, so we draw a hardcoded amount of extra space to
// account for the maximum potential area of the shadow.
// TODO(jsimmons): remove this when Skia does it for us.
canvas.drawRect(
offsetBounds.inflate(20.0),
_RenderPhysicalModelBase._transparentPaint,
);
canvas.drawShadow(
offsetRRectAsPath,
shadowColor,
elevation,
color.alpha != 0xFF,
);
}
canvas.drawRRect(offsetRRect, Paint()..color = color);
context.clipRRectAndPaint(offsetRRect, clipBehavior, offsetBounds, () => super.paint(context, offset));
assert(context.canvas == canvas, 'canvas changed even though needsCompositing was false');
}
final PhysicalModelLayer physicalModel = PhysicalModelLayer(
clipPath: offsetRRectAsPath,
clipBehavior: clipBehavior,
elevation: paintShadows ? elevation : 0.0,
color: color,
shadowColor: shadowColor,
);
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds);
}
}
......@@ -1828,37 +1801,14 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> {
}
return true;
}());
if (needsCompositing) {
final PhysicalModelLayer physicalModel = PhysicalModelLayer(
clipPath: offsetPath,
clipBehavior: clipBehavior,
elevation: paintShadows ? elevation : 0.0,
color: color,
shadowColor: shadowColor,
);
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds);
} else {
final Canvas canvas = context.canvas;
if (elevation != 0.0 && paintShadows) {
// The drawShadow call doesn't add the region of the shadow to the
// picture's bounds, so we draw a hardcoded amount of extra space to
// account for the maximum potential area of the shadow.
// TODO(jsimmons): remove this when Skia does it for us.
canvas.drawRect(
offsetBounds.inflate(20.0),
_RenderPhysicalModelBase._transparentPaint,
);
canvas.drawShadow(
offsetPath,
shadowColor,
elevation,
color.alpha != 0xFF,
);
}
canvas.drawPath(offsetPath, Paint()..color = color..style = PaintingStyle.fill);
context.clipPathAndPaint(offsetPath, clipBehavior, offsetBounds, () => super.paint(context, offset));
assert(context.canvas == canvas, 'canvas changed even though needsCompositing was false');
}
final PhysicalModelLayer physicalModel = PhysicalModelLayer(
clipPath: offsetPath,
clipBehavior: clipBehavior,
elevation: paintShadows ? elevation : 0.0,
color: color,
shadowColor: shadowColor,
);
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds);
}
}
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
......@@ -79,7 +81,7 @@ void main() {
find.byKey(key),
matchesGoldenFile('bottom_app_bar.custom_shape.2.png'),
);
});
}, skip: !Platform.isLinux);
testWidgets('color defaults to Theme.bottomAppBarColor', (WidgetTester tester) async {
await tester.pumpWidget(
......
......@@ -10,6 +10,27 @@ import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
PhysicalModelLayer findPhysicalLayer(Element element) {
expect(element, isNotNull);
RenderObject object = element.renderObject;
while (object != null && object is! RenderRepaintBoundary && object is! RenderView) {
object = object.parent;
}
expect(object.debugLayer, isNotNull);
expect(object.debugLayer.firstChild, isInstanceOf<PhysicalModelLayer>());
final PhysicalModelLayer layer = object.debugLayer.firstChild;
return layer.firstChild is PhysicalModelLayer ? layer.firstChild : layer;
}
void checkPhysicalLayer(Element element, Color expectedColor, {Path clipPath, Rect clipRect}) {
final PhysicalModelLayer expectedLayer = findPhysicalLayer(element);
expect(expectedLayer.elevation, 0.0);
expect(expectedLayer.color, expectedColor);
if (clipPath != null) {
expect(clipRect, isNotNull);
expect(expectedLayer.clipPath, coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)));
}
}
testWidgets('Outline button responds to tap when enabled', (WidgetTester tester) async {
int pressedCount = 0;
......@@ -114,8 +135,13 @@ void main() {
expect(
outlineButton,
paints
..clipPath(pathMatcher: coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)))
..path(color: disabledBorderColor, strokeWidth: borderWidth));
checkPhysicalLayer(
tester.element(outlineButton),
const Color(0),
clipPath: clipPath,
clipRect: clipRect,
);
// Pump a new button with a no-op onPressed callback to make it enabled.
await tester.pumpWidget(
......@@ -130,10 +156,14 @@ void main() {
expect(
outlineButton,
paints
// initially the interior of the button is transparent
..path(color: fillColor.withAlpha(0x00))
..clipPath(pathMatcher: coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)))
..path(color: borderColor, strokeWidth: borderWidth));
// initially, the interior of the button is transparent
checkPhysicalLayer(
tester.element(outlineButton),
fillColor.withAlpha(0x00),
clipPath: clipPath,
clipRect: clipRect,
);
final Offset center = tester.getCenter(outlineButton);
final TestGesture gesture = await tester.startGesture(center);
......@@ -144,9 +174,13 @@ void main() {
expect(
outlineButton,
paints
..path(color: fillColor.withAlpha(0xFF))
..clipPath(pathMatcher: coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)))
..path(color: highlightedBorderColor, strokeWidth: borderWidth));
checkPhysicalLayer(
tester.element(outlineButton),
fillColor.withAlpha(0xFF),
clipPath: clipPath,
clipRect: clipRect,
);
// Tap gesture completes, button returns to its initial configuration.
await gesture.up();
......@@ -154,9 +188,13 @@ void main() {
expect(
outlineButton,
paints
..path(color: fillColor.withAlpha(0x00))
..clipPath(pathMatcher: coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)))
..path(color: borderColor, strokeWidth: borderWidth));
checkPhysicalLayer(
tester.element(outlineButton),
fillColor.withAlpha(0x00),
clipPath: clipPath,
clipRect: clipRect,
);
});
testWidgets('OutlineButton has no clip by default', (WidgetTester tester) async {
......@@ -222,7 +260,6 @@ void main() {
semantics.dispose();
});
testWidgets('OutlineButton scales textScaleFactor', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
......@@ -340,6 +377,7 @@ void main() {
await tester.pumpWidget(buildFrame(ThemeData.dark()));
final Finder button = find.byType(OutlineButton);
final Element buttonElement = tester.element(button);
final Offset center = tester.getCenter(button);
// Default value for dark Theme.of(context).canvasColor as well as
......@@ -347,18 +385,18 @@ void main() {
Color fillColor = Colors.grey[850];
// Initially the interior of the button is transparent.
expect(button, paints..path(color: fillColor.withAlpha(0x00)));
checkPhysicalLayer(buttonElement, fillColor.withAlpha(0x00));
// Tap-press gesture on the button triggers the fill animation.
TestGesture gesture = await tester.startGesture(center);
await tester.pump(); // Start the button fill animation.
await tester.pump(const Duration(milliseconds: 200)); // Animation is complete.
expect(button, paints..path(color: fillColor.withAlpha(0xFF)));
checkPhysicalLayer(buttonElement, fillColor.withAlpha(0xFF));
// Tap gesture completes, button returns to its initial configuration.
await gesture.up();
await tester.pumpAndSettle();
expect(button, paints..path(color: fillColor.withAlpha(0x00)));
checkPhysicalLayer(buttonElement, fillColor.withAlpha(0x00));
await tester.pumpWidget(buildFrame(ThemeData.light()));
await tester.pumpAndSettle(); // Finish the theme change animation.
......@@ -368,17 +406,17 @@ void main() {
fillColor = Colors.grey[50];
// Initially the interior of the button is transparent.
expect(button, paints..path(color: fillColor.withAlpha(0x00)));
// expect(button, paints..path(color: fillColor.withAlpha(0x00)));
// Tap-press gesture on the button triggers the fill animation.
gesture = await tester.startGesture(center);
await tester.pump(); // Start the button fill animation.
await tester.pump(const Duration(milliseconds: 200)); // Animation is complete.
expect(button, paints..path(color: fillColor.withAlpha(0xFF)));
checkPhysicalLayer(buttonElement, fillColor.withAlpha(0xFF));
// Tap gesture completes, button returns to its initial configuration.
await gesture.up();
await tester.pumpAndSettle();
expect(button, paints..path(color: fillColor.withAlpha(0x00)));
checkPhysicalLayer(buttonElement, fillColor.withAlpha(0x00));
});
}
......@@ -42,7 +42,7 @@ void main() {
final RenderPhysicalModel root = RenderPhysicalModel(color: const Color(0xffff00ff));
layout(root, phase: EnginePhase.composite);
expect(root.needsCompositing, isFalse);
expect(root.needsCompositing, isTrue);
// On Fuchsia, the system compositor is responsible for drawing shadows
// for physical model layers with non-zero elevation.
......@@ -52,7 +52,7 @@ void main() {
root.elevation = 0.0;
pumpFrame(phase: EnginePhase.composite);
expect(root.needsCompositing, isFalse);
expect(root.needsCompositing, isTrue);
debugDefaultTargetPlatformOverride = null;
});
......@@ -62,16 +62,16 @@ void main() {
final RenderPhysicalModel root = RenderPhysicalModel(color: const Color(0xffff00ff));
layout(root, phase: EnginePhase.composite);
expect(root.needsCompositing, isFalse);
expect(root.needsCompositing, isTrue);
// On non-Fuchsia platforms, Flutter draws its own shadows.
// Flutter now composites physical shapes on all platforms.
root.elevation = 1.0;
pumpFrame(phase: EnginePhase.composite);
expect(root.needsCompositing, isFalse);
expect(root.needsCompositing, isTrue);
root.elevation = 0.0;
pumpFrame(phase: EnginePhase.composite);
expect(root.needsCompositing, isFalse);
expect(root.needsCompositing, isTrue);
debugDefaultTargetPlatformOverride = null;
});
......@@ -125,16 +125,16 @@ void main() {
clipper: const ShapeBorderClipper(shape: CircleBorder()),
);
layout(root, phase: EnginePhase.composite);
expect(root.needsCompositing, isFalse);
expect(root.needsCompositing, isTrue);
// On non-Fuchsia platforms, Flutter draws its own shadows.
// On non-Fuchsia platforms, we composite physical shape layers
root.elevation = 1.0;
pumpFrame(phase: EnginePhase.composite);
expect(root.needsCompositing, isFalse);
expect(root.needsCompositing, isTrue);
root.elevation = 0.0;
pumpFrame(phase: EnginePhase.composite);
expect(root.needsCompositing, isFalse);
expect(root.needsCompositing, isTrue);
debugDefaultTargetPlatformOverride = null;
});
......
......@@ -6,8 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import '../rendering/mock_canvas.dart';
import 'package:flutter/rendering.dart';
class _CustomPhysics extends ClampingScrollPhysics {
const _CustomPhysics({ ScrollPhysics parent }) : super(parent: parent);
......@@ -469,12 +468,38 @@ void main() {
// END
)),
);
PhysicalModelLayer _dfsFindPhysicalLayer(ContainerLayer layer) {
expect(layer, isNotNull);
Layer child = layer.firstChild;
while (child != null) {
if (child is PhysicalModelLayer) {
return child;
}
if (child is ContainerLayer) {
final PhysicalModelLayer candidate = _dfsFindPhysicalLayer(child);
if (candidate != null) {
return candidate;
}
}
child = child.nextSibling;
}
return null;
}
final ContainerLayer nestedScrollViewLayer = find.byType(NestedScrollView).evaluate().first.renderObject.debugLayer;
void _checkPhysicalLayer({@required double elevation}) {
final PhysicalModelLayer layer = _dfsFindPhysicalLayer(nestedScrollViewLayer);
expect(layer, isNotNull);
expect(layer.elevation, equals(elevation));
}
int expectedBuildCount = 0;
expectedBuildCount += 1;
expect(buildCount, expectedBuildCount);
expect(find.text('Item 2'), findsOneWidget);
expect(find.text('Item 18'), findsNothing);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
_checkPhysicalLayer(elevation: 0);
// scroll down
final TestGesture gesture0 = await tester.startGesture(tester.getCenter(find.text('Item 2')));
await gesture0.moveBy(const Offset(0.0, -120.0)); // tiny bit more than the pinned app bar height (56px * 2)
......@@ -488,22 +513,22 @@ void main() {
expect(buildCount, expectedBuildCount);
await tester.pump(const Duration(milliseconds: 1)); // during shadow animation
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
_checkPhysicalLayer(elevation: 0.00018262863159179688);
await tester.pump(const Duration(seconds: 1)); // end shadow animation
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
_checkPhysicalLayer(elevation: 4);
// scroll down
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('Item 2')));
await gesture1.moveBy(const Offset(0.0, -800.0));
await tester.pump();
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
_checkPhysicalLayer(elevation: 4);
expect(find.text('Item 2'), findsNothing);
expect(find.text('Item 18'), findsOneWidget);
await gesture1.up();
await tester.pump(const Duration(seconds: 1));
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
_checkPhysicalLayer(elevation: 4);
// swipe left to bring in tap on the right
final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
await gesture2.moveBy(const Offset(-400.0, 0.0));
......@@ -514,7 +539,7 @@ void main() {
expect(find.text('Item 0'), findsOneWidget);
expect(tester.getTopLeft(find.ancestor(of: find.text('Item 0'), matching: find.byType(ListTile))).dy,
tester.getBottomLeft(find.byType(AppBar)).dy + 8.0);
expect(find.byType(NestedScrollView), paints..shadow());
_checkPhysicalLayer(elevation: 4);
await gesture2.up();
await tester.pump(); // start sideways scroll
await tester.pump(const Duration(seconds: 1)); // end sideways scroll, triggers shadow going away
......@@ -526,7 +551,7 @@ void main() {
expect(buildCount, expectedBuildCount);
expect(find.text('Item 18'), findsNothing);
expect(find.text('Item 2'), findsOneWidget);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
_checkPhysicalLayer(elevation: 0);
await tester.pump(const Duration(seconds: 1)); // just checking we don't rebuild...
expect(buildCount, expectedBuildCount);
// peek left to see it's still in the right place
......@@ -539,10 +564,10 @@ void main() {
expect(buildCount, expectedBuildCount);
expect(find.text('Item 18'), findsOneWidget);
expect(find.text('Item 2'), findsOneWidget);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
_checkPhysicalLayer(elevation: 0);
await tester.pump(const Duration(seconds: 1)); // shadow finishes coming back
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
_checkPhysicalLayer(elevation: 4);
await gesture3.moveBy(const Offset(-400.0, 0.0));
await gesture3.up();
await tester.pump(); // left tab view goes away
......@@ -550,10 +575,10 @@ void main() {
await tester.pump(); // shadow goes away starting here
expectedBuildCount += 1;
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), paints..shadow());
_checkPhysicalLayer(elevation: 4);
await tester.pump(const Duration(seconds: 1)); // shadow finishes going away
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
_checkPhysicalLayer(elevation: 0);
// scroll back up
final TestGesture gesture4 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
await gesture4.moveBy(const Offset(0.0, 200.0)); // expands the appbar again
......@@ -561,11 +586,11 @@ void main() {
expect(buildCount, expectedBuildCount);
expect(find.text('Item 2'), findsOneWidget);
expect(find.text('Item 18'), findsNothing);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
_checkPhysicalLayer(elevation: 0);
await gesture4.up();
await tester.pump(const Duration(seconds: 1));
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
_checkPhysicalLayer(elevation: 0);
// peek left to see it's now back at zero
final TestGesture gesture5 = await tester.startGesture(tester.getCenter(find.byType(NestedScrollView)));
await gesture5.moveBy(const Offset(400.0, 0.0));
......@@ -574,14 +599,14 @@ void main() {
expect(buildCount, expectedBuildCount);
expect(find.text('Item 18'), findsNothing);
expect(find.text('Item 2'), findsNWidgets(2));
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
_checkPhysicalLayer(elevation: 0);
await tester.pump(const Duration(seconds: 1)); // shadow would be finished coming back
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
_checkPhysicalLayer(elevation: 0);
await gesture5.up();
await tester.pump(); // right tab view goes away
await tester.pumpAndSettle();
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
_checkPhysicalLayer(elevation: 0);
debugDisableShadows = true;
});
......
......@@ -36,4 +36,31 @@ void main() {
expect(physicalModelLayer.elevation, 1.0);
debugDisableShadows = true;
});
testWidgets('PhysicalModel - clips when overflows and elevation is 0', (WidgetTester tester) async {
const Key key = Key('test');
await tester.pumpWidget(
MediaQuery(
key: key,
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Padding(
padding: const EdgeInsets.all(50),
child: Row(
children: const <Widget>[
Material(child: Text('A long long long long long long long string')),
Material(child: Text('A long long long long long long long string')),
Material(child: Text('A long long long long long long long string')),
Material(child: Text('A long long long long long long long string')),
],
),
),
),
),
);
expect(tester.takeException(), startsWith('A RenderFlex overflowed by '));
expect(find.byKey(key), matchesGoldenFile('physical_model_overflow.png'));
});
}
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