From a13f7717bf332f6d1857013716a6a1edceec057f Mon Sep 17 00:00:00 2001 From: Conner Kasten Date: Mon, 31 Mar 2025 16:13:32 -0400 Subject: [PATCH] Public nodes needing paint or layout (#166148) This PR adds 2 new `@protected` fields to `PipelineOwner` which list the RenderObjects currently needing paint or layout for the next frame. This PR addresses https://github.com/flutter/flutter/issues/166147, which has further discussion of the motivation. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/lib/src/rendering/object.dart | 28 ++++++++++++++ .../flutter/test/rendering/object_test.dart | 38 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 868b05100a..9e560d4532 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -1088,6 +1088,21 @@ base class PipelineOwner with DiagnosticableTreeMixin { bool _shouldMergeDirtyNodes = false; List _nodesNeedingLayout = []; + /// The [RenderObject]s representing relayout boundaries which need to be laid out + /// in the next [flushLayout] pass. + /// + /// Relayout boundaries are added when they are marked for layout. + /// Subclasses of [PipelineOwner] may use them to invalidate caches or + /// otherwise make performance optimizations. Since nodes may be marked for + /// layout at any time, they are best checked during [flushLayout]. + /// + /// Relayout boundaries owned by child [PipelineOwner]s are not included here. + /// + /// Boundaries appear in an arbitrary order, and may appear multiple times. + @protected + @nonVirtual + Iterable get nodesNeedingLayout => _nodesNeedingLayout; + /// Whether this pipeline is currently in the layout phase. /// /// Specifically, whether [flushLayout] is currently running. @@ -1232,6 +1247,19 @@ base class PipelineOwner with DiagnosticableTreeMixin { List _nodesNeedingPaint = []; + /// The [RenderObject]s which need to be painted in the next [flushPaint] pass. + /// + /// [RenderObject]s marked with [RenderObject.isRepaintBoundary] are added + /// when they are marked needing paint. Subclasses of [PipelineOwner] may use them + /// to invalidate caches or otherwise make performance optimizations. + /// Since nodes may be marked for layout at any time, they are best checked during + /// [flushPaint]. + /// + /// Marked children of child [PipelineOwner]s are not included here. + @protected + @nonVirtual + Iterable get nodesNeedingPaint => _nodesNeedingPaint; + /// Whether this pipeline is currently in the paint phase. /// /// Specifically, whether [flushPaint] is currently running. diff --git a/packages/flutter/test/rendering/object_test.dart b/packages/flutter/test/rendering/object_test.dart index 5ac2fd7af8..05531f42ab 100644 --- a/packages/flutter/test/rendering/object_test.dart +++ b/packages/flutter/test/rendering/object_test.dart @@ -22,6 +22,37 @@ void main() { ); }); + test('nodesNeedingLayout updated with layout changes', () { + final _TestPipelineOwner owner = _TestPipelineOwner(); + final TestRenderObject renderObject = TestRenderObject()..isRepaintBoundary = true; + renderObject.attach(owner); + expect(owner.needLayout, isEmpty); + + renderObject.layout(const BoxConstraints.tightForFinite()); + renderObject.markNeedsLayout(); + expect(owner.needLayout, contains(renderObject)); + + owner.flushLayout(); + expect(owner.needLayout, isEmpty); + }); + + test('nodesNeedingPaint updated with paint changes', () { + final _TestPipelineOwner owner = _TestPipelineOwner(); + final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true) + ..isRepaintBoundary = true; + final OffsetLayer layer = OffsetLayer(); + layer.attach(owner); + renderObject.attach(owner); + expect(owner.needPaint, isEmpty); + + renderObject.markNeedsPaint(); + renderObject.scheduleInitialPaint(layer); + expect(owner.needPaint, contains(renderObject)); + + owner.flushPaint(); + expect(owner.needPaint, isEmpty); + }); + test('ensure frame is scheduled for markNeedsSemanticsUpdate', () { // Initialize all bindings because owner.flushSemantics() requires a window final TestRenderObject renderObject = TestRenderObject(); @@ -686,3 +717,10 @@ class TestThrowingRenderObject extends RenderObject { return Rect.zero; } } + +final class _TestPipelineOwner extends PipelineOwner { + // Make these protected fields visible for testing. + Iterable get needLayout => super.nodesNeedingLayout; + + Iterable get needPaint => super.nodesNeedingPaint; +}