diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 14ac8dbc4a298..88ac9a0bda531 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -890,6 +890,9 @@ class RenderOpacity extends RenderProxyBox { @override bool get alwaysNeedsCompositing => child != null && _alpha > 0; + @override + bool get isRepaintBoundary => alwaysNeedsCompositing; + int _alpha; /// The fraction to scale the child's alpha value. @@ -917,7 +920,7 @@ class RenderOpacity extends RenderProxyBox { if (didNeedCompositing != alwaysNeedsCompositing) { markNeedsCompositingBitsUpdate(); } - markNeedsPaint(); + markNeedsCompositedLayerUpdate(); if (wasVisible != (_alpha != 0) && !alwaysIncludeSemantics) { markNeedsSemanticsUpdate(); } @@ -944,23 +947,19 @@ class RenderOpacity extends RenderProxyBox { return _alpha > 0; } + @override + OffsetLayer updateCompositedLayer({required covariant OpacityLayer? oldLayer}) { + final OpacityLayer layer = oldLayer ?? OpacityLayer(); + layer.alpha = _alpha; + return layer; + } + @override void paint(PaintingContext context, Offset offset) { - if (child == null) { - return; - } - if (_alpha == 0) { - // No need to keep the layer. We'll create a new one if necessary. - layer = null; + if (child == null || _alpha == 0) { return; } - - assert(needsCompositing); - layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?); - assert(() { - layer!.debugCreator = debugCreator; - return true; - }()); + super.paint(context, offset); } @override diff --git a/packages/flutter/test/widgets/opacity_repaint_test.dart b/packages/flutter/test/widgets/opacity_repaint_test.dart new file mode 100644 index 0000000000000..c285075c5a57b --- /dev/null +++ b/packages/flutter/test/widgets/opacity_repaint_test.dart @@ -0,0 +1,96 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('RenderOpacity avoids repainting and does not drop layer at fully opaque', (WidgetTester tester) async { + RenderTestObject.paintCount = 0; + await tester.pumpWidget( + Container( + color: Colors.red, + child: const Opacity( + opacity: 0.0, + child: TestWidget(), + ), + ) + ); + + expect(RenderTestObject.paintCount, 0); + + await tester.pumpWidget( + Container( + color: Colors.red, + child: const Opacity( + opacity: 0.1, + child: TestWidget(), + ), + ) + ); + + expect(RenderTestObject.paintCount, 1); + + await tester.pumpWidget( + Container( + color: Colors.red, + child: const Opacity( + opacity: 1, + child: TestWidget(), + ), + ) + ); + + expect(RenderTestObject.paintCount, 1); + }); + + testWidgets('RenderOpacity allows opacity layer to be dropped at 0 opacity', (WidgetTester tester) async { + RenderTestObject.paintCount = 0; + + await tester.pumpWidget( + Container( + color: Colors.red, + child: const Opacity( + opacity: 0.5, + child: TestWidget(), + ), + ) + ); + + expect(RenderTestObject.paintCount, 1); + + await tester.pumpWidget( + Container( + color: Colors.red, + child: const Opacity( + opacity: 0.0, + child: TestWidget(), + ), + ) + ); + + expect(RenderTestObject.paintCount, 1); + expect(tester.layers, isNot(contains(isA()))); + }); +} + +class TestWidget extends SingleChildRenderObjectWidget { + const TestWidget({super.key, super.child}); + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderTestObject(); + } +} + +class RenderTestObject extends RenderProxyBox { + static int paintCount = 0; + + @override + void paint(PaintingContext context, Offset offset) { + paintCount += 1; + super.paint(context, offset); + } +}