From a63ee24b17580e8236ea84085cf39db9851c32b0 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Fri, 25 Mar 2022 16:20:14 -0700 Subject: [PATCH] Reland "Avoid calling `performLayout` when only the relayout boundary is different" (#100581) --- .../flutter/lib/src/rendering/object.dart | 44 ++++++++++--- .../rendering/relayout_boundary_test.dart | 61 +++++++++++++++++++ 2 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 packages/flutter/test/rendering/relayout_boundary_test.dart diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 0f7ef628f7..3c176c5793 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -1629,7 +1629,16 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im assert(_debugSubtreeRelayoutRootAlreadyMarkedNeedsLayout()); return; } - assert(_relayoutBoundary != null); + if (_relayoutBoundary == null) { + _needsLayout = true; + if (parent != null) { + // _relayoutBoundary is cleaned by an ancestor in RenderObject.layout. + // Conservatively mark everything dirty until it reaches the closest + // known relayout boundary. + markParentNeedsLayout(); + } + return; + } if (_relayoutBoundary != this) { markParentNeedsLayout(); } else { @@ -1683,16 +1692,31 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im void _cleanRelayoutBoundary() { if (_relayoutBoundary != this) { _relayoutBoundary = null; - _needsLayout = true; visitChildren(_cleanChildRelayoutBoundary); } } + void _propagateRelayoutBoundary() { + if (_relayoutBoundary == this) { + return; + } + final RenderObject? parentRelayoutBoundary = (parent as RenderObject?)?._relayoutBoundary; + assert(parentRelayoutBoundary != null); + if (parentRelayoutBoundary != _relayoutBoundary) { + _relayoutBoundary = parentRelayoutBoundary; + visitChildren(_propagateRelayoutBoundaryToChild); + } + } + // Reduces closure allocation for visitChildren use cases. static void _cleanChildRelayoutBoundary(RenderObject child) { child._cleanRelayoutBoundary(); } + static void _propagateRelayoutBoundaryToChild(RenderObject child) { + child._propagateRelayoutBoundary(); + } + /// Bootstrap the rendering pipeline by scheduling the very first layout. /// /// Requires this render object to be attached and that this render object @@ -1814,17 +1838,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im )); assert(!_debugDoingThisResize); assert(!_debugDoingThisLayout); - RenderObject? relayoutBoundary; - if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) { - relayoutBoundary = this; - } else { - relayoutBoundary = (parent! as RenderObject)._relayoutBoundary; - } + final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject; + final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!; assert(() { _debugCanParentUseSize = parentUsesSize; return true; }()); - if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) { + + if (!_needsLayout && constraints == _constraints) { assert(() { // in case parentUsesSize changed since the last invocation, set size // to itself, so it has the right internal debug values. @@ -1839,6 +1860,11 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im return true; }()); + if (relayoutBoundary != _relayoutBoundary) { + _relayoutBoundary = relayoutBoundary; + visitChildren(_propagateRelayoutBoundaryToChild); + } + if (!kReleaseMode && debugProfileLayoutsEnabled) Timeline.finishSync(); return; diff --git a/packages/flutter/test/rendering/relayout_boundary_test.dart b/packages/flutter/test/rendering/relayout_boundary_test.dart new file mode 100644 index 0000000000..1823f1ad69 --- /dev/null +++ b/packages/flutter/test/rendering/relayout_boundary_test.dart @@ -0,0 +1,61 @@ +// 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/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('relayout boundary change does not trigger relayout', (WidgetTester tester) async { + final RenderLayoutCount renderLayoutCount = RenderLayoutCount(); + final Widget layoutCounter = Center( + key: GlobalKey(), + child: WidgetToRenderBoxAdapter(renderBox: renderLayoutCount), + ); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 100, + height: 100, + child: Center( + child: SizedBox( + width: 100, + height: 100, + child: Center( + child: layoutCounter, + ), + ), + ), + ), + ), + ); + + expect(renderLayoutCount.layoutCount, 1); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 100, + height: 100, + child: layoutCounter, + ), + ), + ); + + expect(renderLayoutCount.layoutCount, 1); + }); +} + +// This class is needed because LayoutBuilder's RenderObject does not always +// call the builder method in its PerformLayout method. +class RenderLayoutCount extends RenderBox { + int layoutCount = 0; + + @override + void performLayout() { + layoutCount += 1; + size = constraints.biggest; + } +}