Skip to content

Commit d9ef7df

Browse files
authored
Use RRects instead of Paths when possible in Material. (flutter#14404)
0672055 changed the Material widget to always use Paths for representing the outline. These paths are later used for clipping and drawing a shadow. This changed introduced a performance regression, see: flutter#14403 We did not expect a path that is a rounded rectangle to be less performant than a rounded rectangle, as Skia should be able to tell the path is just a rounded rectangle. Until we find a solution for this regression, we keep using RRect when we can represent the shape with it.
1 parent b54b576 commit d9ef7df

File tree

4 files changed

+67
-16
lines changed

4 files changed

+67
-16
lines changed

packages/flutter/lib/src/material/material.dart

+43
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,14 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
295295

296296
if (widget.type == MaterialType.transparency)
297297
return _clipToShape(shape: shape, contents: contents);
298+
299+
// PhysicalModel performs better than PhysicalShape, so we use it when
300+
// possible.
301+
// This is not expected, and we do this as a temporary workaround until the
302+
// shape performance regression is resolved, see:
303+
// https://github.com/flutter/flutter/issues/14403
304+
if (shape.runtimeType == CircleBorder || shape.runtimeType == RoundedRectangleBorder)
305+
return _physicalModelInterior(contents, shape, backgroundColor);
298306

299307
return new _MaterialInterior(
300308
curve: Curves.fastOutSlowIn,
@@ -305,10 +313,45 @@ class _MaterialState extends State<Material> with TickerProviderStateMixin {
305313
shadowColor: widget.shadowColor,
306314
child: contents,
307315
);
316+
}
308317

318+
Widget _physicalModelInterior(Widget contents, ShapeBorder shape, Color backgroundColor) {
319+
assert(shape.runtimeType == CircleBorder || shape.runtimeType == RoundedRectangleBorder);
320+
BoxShape boxShape;
321+
BorderRadius borderRadius;
322+
if (shape.runtimeType == CircleBorder) {
323+
boxShape = BoxShape.circle;
324+
borderRadius = BorderRadius.zero;
325+
} else {
326+
final RoundedRectangleBorder border = shape;
327+
boxShape = BoxShape.rectangle;
328+
borderRadius = border.borderRadius;
329+
}
330+
return new AnimatedPhysicalModel(
331+
curve: Curves.fastOutSlowIn,
332+
duration: kThemeChangeDuration,
333+
shape: boxShape,
334+
borderRadius: borderRadius,
335+
elevation: widget.elevation,
336+
color: backgroundColor,
337+
shadowColor: widget.shadowColor,
338+
animateColor: false,
339+
child: contents,
340+
);
309341
}
310342

311343
static Widget _clipToShape({ShapeBorder shape, Widget contents}) {
344+
// ClipRRect performs better than ClipPath, so we use it when possible.
345+
// This is not expected, and we do this as a temporary workaround until the
346+
// shape performance regression is resolved, see:
347+
// https://github.com/flutter/flutter/issues/14403
348+
if (shape.runtimeType == RoundedRectangleBorder) {
349+
final RoundedRectangleBorder border = shape;
350+
return new ClipRRect(
351+
borderRadius: border.borderRadius,
352+
child: contents,
353+
);
354+
}
312355
return new ClipPath(
313356
child: contents,
314357
clipper: new ShapeBorderClipper(

packages/flutter/test/material/ink_well_test.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -140,17 +140,17 @@ void main() {
140140
),
141141
),
142142
);
143-
expect(tester.renderObject<RenderProxyBox>(find.byType(PhysicalShape)).child, paintsNothing);
143+
expect(tester.renderObject<RenderProxyBox>(find.byType(PhysicalModel)).child, paintsNothing);
144144
await tester.tap(find.byType(InkWell));
145145
await tester.pump();
146146
await tester.pump(const Duration(milliseconds: 10));
147-
expect(tester.renderObject<RenderProxyBox>(find.byType(PhysicalShape)).child, paints..circle());
147+
expect(tester.renderObject<RenderProxyBox>(find.byType(PhysicalModel)).child, paints..circle());
148148
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
149149
await tester.pump(const Duration(milliseconds: 10));
150150
await tester.drag(find.byType(ListView), const Offset(0.0, 1000.0));
151151
await tester.pump(const Duration(milliseconds: 10));
152152
expect(
153-
tester.renderObject<RenderProxyBox>(find.byType(PhysicalShape)).child,
153+
tester.renderObject<RenderProxyBox>(find.byType(PhysicalModel)).child,
154154
keepAlive ? (paints..circle()) : paintsNothing,
155155
);
156156
}

packages/flutter/test/material/material_test.dart

+13-13
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ Widget buildMaterial(
2929
);
3030
}
3131

32-
RenderPhysicalShape getShadow(WidgetTester tester) {
33-
return tester.renderObject(find.byType(PhysicalShape));
32+
RenderPhysicalModel getShadow(WidgetTester tester) {
33+
return tester.renderObject(find.byType(PhysicalModel));
3434
}
3535

3636
class PaintRecorder extends CustomPainter {
@@ -122,23 +122,23 @@ void main() {
122122
// a kThemeChangeDuration time interval.
123123

124124
await tester.pumpWidget(buildMaterial(elevation: 0.0));
125-
final RenderPhysicalShape modelA = getShadow(tester);
125+
final RenderPhysicalModel modelA = getShadow(tester);
126126
expect(modelA.elevation, equals(0.0));
127127

128128
await tester.pumpWidget(buildMaterial(elevation: 9.0));
129-
final RenderPhysicalShape modelB = getShadow(tester);
129+
final RenderPhysicalModel modelB = getShadow(tester);
130130
expect(modelB.elevation, equals(0.0));
131131

132132
await tester.pump(const Duration(milliseconds: 1));
133-
final RenderPhysicalShape modelC = getShadow(tester);
133+
final RenderPhysicalModel modelC = getShadow(tester);
134134
expect(modelC.elevation, closeTo(0.0, 0.001));
135135

136136
await tester.pump(kThemeChangeDuration ~/ 2);
137-
final RenderPhysicalShape modelD = getShadow(tester);
137+
final RenderPhysicalModel modelD = getShadow(tester);
138138
expect(modelD.elevation, isNot(closeTo(0.0, 0.001)));
139139

140140
await tester.pump(kThemeChangeDuration);
141-
final RenderPhysicalShape modelE = getShadow(tester);
141+
final RenderPhysicalModel modelE = getShadow(tester);
142142
expect(modelE.elevation, equals(9.0));
143143
});
144144

@@ -147,23 +147,23 @@ void main() {
147147
// a kThemeChangeDuration time interval.
148148

149149
await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFF00FF00)));
150-
final RenderPhysicalShape modelA = getShadow(tester);
150+
final RenderPhysicalModel modelA = getShadow(tester);
151151
expect(modelA.shadowColor, equals(const Color(0xFF00FF00)));
152152

153153
await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFFFF0000)));
154-
final RenderPhysicalShape modelB = getShadow(tester);
154+
final RenderPhysicalModel modelB = getShadow(tester);
155155
expect(modelB.shadowColor, equals(const Color(0xFF00FF00)));
156156

157157
await tester.pump(const Duration(milliseconds: 1));
158-
final RenderPhysicalShape modelC = getShadow(tester);
158+
final RenderPhysicalModel modelC = getShadow(tester);
159159
expect(modelC.shadowColor, within<Color>(distance: 1, from: const Color(0xFF00FF00)));
160160

161161
await tester.pump(kThemeChangeDuration ~/ 2);
162-
final RenderPhysicalShape modelD = getShadow(tester);
162+
final RenderPhysicalModel modelD = getShadow(tester);
163163
expect(modelD.shadowColor, isNot(within<Color>(distance: 1, from: const Color(0xFF00FF00))));
164164

165165
await tester.pump(kThemeChangeDuration);
166-
final RenderPhysicalShape modelE = getShadow(tester);
166+
final RenderPhysicalModel modelE = getShadow(tester);
167167
expect(modelE.shadowColor, equals(const Color(0xFFFF0000)));
168168
});
169169

@@ -178,7 +178,7 @@ void main() {
178178
)
179179
);
180180

181-
expect(find.byKey(materialKey), clipsWithBoundingRect);
181+
expect(find.byKey(materialKey), clipsWithBoundingRRect(borderRadius: BorderRadius.zero));
182182
});
183183

184184
testWidgets('clips to rounded rect when borderRadius provided', (WidgetTester tester) async {

packages/flutter/test/rendering/recording_canvas.dart

+8
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ class TestRecordingPaintingContext implements PaintingContext {
110110
canvas.restore();
111111
}
112112

113+
@override
114+
void pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter) {
115+
canvas.save();
116+
canvas.clipRRect(clipRRect.shift(offset));
117+
painter(this, offset);
118+
canvas.restore();
119+
}
120+
113121
@override
114122
void pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter) {
115123
canvas

0 commit comments

Comments
 (0)