enable more retained rendering opportunities for LeaderLayer (#96486)
diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart
index eee4ee8..73310e7 100644
--- a/packages/flutter/lib/src/rendering/layer.dart
+++ b/packages/flutter/lib/src/rendering/layer.dart
@@ -2109,6 +2109,8 @@
/// * [RenderLeaderLayer] and [RenderFollowerLayer], the corresponding
/// render objects.
class LayerLink {
+ /// The [LeaderLayer] connected to this link.
+ LeaderLayer? get leader => _leader;
LeaderLayer? _leader;
void _registerLeader(LeaderLayer leader) {
@@ -2160,35 +2162,6 @@
}());
}
- int _connectedFollowers = 0;
-
- /// Whether a [LeaderLayer] is currently connected to this link.
- bool get leaderConnected => _leader != null;
-
- /// Called by the [FollowerLayer] to establish a link to a [LeaderLayer].
- ///
- /// The returned [LayerLinkHandle] provides access to the leader via
- /// [LayerLinkHandle.leader].
- ///
- /// When the [FollowerLayer] no longer wants to follow the [LeaderLayer],
- /// [LayerLinkHandle.dispose] must be called to disconnect the link.
- _LayerLinkHandle _registerFollower() {
- assert(_connectedFollowers >= 0);
- _connectedFollowers++;
- return _LayerLinkHandle(this);
- }
-
- /// Returns the [LeaderLayer] currently connected to this link.
- ///
- /// Valid in debug mode only. Returns null in all other modes.
- LeaderLayer? get debugLeader {
- LeaderLayer? result;
- if (kDebugMode) {
- result = _leader;
- }
- return result;
- }
-
/// The total size of the content of the connected [LeaderLayer].
///
/// Generally this should be set by the [RenderObject] that paints on the
@@ -2201,30 +2174,6 @@
String toString() => '${describeIdentity(this)}(${ _leader != null ? "<linked>" : "<dangling>" })';
}
-/// A handle provided by [LayerLink.registerFollower] to a calling
-/// [FollowerLayer] to establish a link between that [FollowerLayer] and a
-/// [LeaderLayer].
-///
-/// If the link is no longer needed, [dispose] must be called to disconnect it.
-class _LayerLinkHandle {
- _LayerLinkHandle(this._link);
-
- LayerLink? _link;
-
- /// The currently-registered [LeaderLayer], if any.
- LeaderLayer? get leader => _link!._leader;
-
- /// Disconnects the link between the [FollowerLayer] owning this handle and
- /// the [leader].
- ///
- /// The [LayerLinkHandle] becomes unusable after calling this method.
- void dispose() {
- assert(_link!._connectedFollowers > 0);
- _link!._connectedFollowers--;
- _link = null;
- }
-}
-
/// A composited layer that can be followed by a [FollowerLayer].
///
/// This layer collapses the accumulated offset into a transform and passes
@@ -2279,41 +2228,18 @@
}
}
- /// {@macro flutter.rendering.FollowerLayer.alwaysNeedsAddToScene}
- @override
- bool get alwaysNeedsAddToScene => _link._connectedFollowers > 0;
-
@override
void attach(Object owner) {
super.attach(owner);
- assert(_debugSetLastOffset(null));
_link._registerLeader(this);
}
@override
void detach() {
- assert(_debugSetLastOffset(null));
_link._unregisterLeader(this);
super.detach();
}
- /// The offset the last time this layer was composited.
- ///
- /// This is reset to null when the layer is attached or detached, to help
- /// catch cases where the follower layer ends up before the leader layer, but
- /// not every case can be detected.
- Offset? _debugLastOffset;
-
- bool _debugSetLastOffset(Offset? offset) {
- bool result = false;
- assert(() {
- _debugLastOffset = offset;
- result = true;
- return true;
- }());
- return result;
- }
-
@override
bool findAnnotations<S extends Object>(AnnotationResult<S> result, Offset localPosition, { required bool onlyFirst }) {
return super.findAnnotations<S>(result, localPosition - offset, onlyFirst: onlyFirst);
@@ -2322,7 +2248,6 @@
@override
void addToScene(ui.SceneBuilder builder) {
assert(offset != null);
- assert(_debugSetLastOffset(offset));
if (offset != Offset.zero)
engineLayer = builder.pushTransform(
Matrix4.translationValues(offset.dx, offset.dy, 0.0).storage,
@@ -2386,10 +2311,6 @@
LayerLink get link => _link;
set link(LayerLink value) {
assert(value != null);
- if (value != _link && _leaderHandle != null) {
- _leaderHandle!.dispose();
- _leaderHandle = value._registerFollower();
- }
_link = value;
}
LayerLink _link;
@@ -2436,21 +2357,6 @@
/// * [unlinkedOffset], for when the layer is not linked.
Offset? linkedOffset;
- _LayerLinkHandle? _leaderHandle;
-
- @override
- void attach(Object owner) {
- super.attach(owner);
- _leaderHandle = _link._registerFollower();
- }
-
- @override
- void detach() {
- super.detach();
- _leaderHandle?.dispose();
- _leaderHandle = null;
- }
-
Offset? _lastOffset;
Matrix4? _lastTransform;
Matrix4? _invertedTransform;
@@ -2470,7 +2376,7 @@
@override
bool findAnnotations<S extends Object>(AnnotationResult<S> result, Offset localPosition, { required bool onlyFirst }) {
- if (_leaderHandle!.leader == null) {
+ if (_link.leader == null) {
if (showWhenUnlinked!) {
return super.findAnnotations(result, localPosition - unlinkedOffset!, onlyFirst: onlyFirst);
}
@@ -2545,11 +2451,39 @@
return _pathsToCommonAncestor(a.parent, b.parent, ancestorsA, ancestorsB);
}
+ bool _debugCheckLeaderBeforeFollower(
+ List<ContainerLayer> leaderToCommonAncestor,
+ List<ContainerLayer> followerToCommonAncestor,
+ ) {
+ if (followerToCommonAncestor.length <= 1) {
+ // Follower is the common ancestor, ergo the leader must come AFTER the follower.
+ return false;
+ }
+ if (leaderToCommonAncestor.length <= 1) {
+ // Leader is the common ancestor, ergo the leader must come BEFORE the follower.
+ return true;
+ }
+
+ // Common ancestor is neither the leader nor the follower.
+ final ContainerLayer leaderSubtreeBelowAncestor = leaderToCommonAncestor[leaderToCommonAncestor.length - 2];
+ final ContainerLayer followerSubtreeBelowAncestor = followerToCommonAncestor[followerToCommonAncestor.length - 2];
+
+ Layer? sibling = leaderSubtreeBelowAncestor;
+ while (sibling != null) {
+ if (sibling == followerSubtreeBelowAncestor) {
+ return true;
+ }
+ sibling = sibling.nextSibling;
+ }
+ // The follower subtree didn't come after the leader subtree.
+ return false;
+ }
+
/// Populate [_lastTransform] given the current state of the tree.
void _establishTransform() {
assert(link != null);
_lastTransform = null;
- final LeaderLayer? leader = _leaderHandle!.leader;
+ final LeaderLayer? leader = _link.leader;
// Check to see if we are linked.
if (leader == null)
return;
@@ -2558,22 +2492,25 @@
leader.owner == owner,
'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.',
);
- assert(
- leader._debugLastOffset != null,
- 'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.',
- );
// Stores [leader, ..., commonAncestor] after calling _pathsToCommonAncestor.
- final List<ContainerLayer?> forwardLayers = <ContainerLayer>[leader];
+ final List<ContainerLayer> forwardLayers = <ContainerLayer>[leader];
// Stores [this (follower), ..., commonAncestor] after calling
// _pathsToCommonAncestor.
- final List<ContainerLayer?> inverseLayers = <ContainerLayer>[this];
+ final List<ContainerLayer> inverseLayers = <ContainerLayer>[this];
final Layer? ancestor = _pathsToCommonAncestor(
leader, this,
forwardLayers, inverseLayers,
);
- assert(ancestor != null);
+ assert(
+ ancestor != null,
+ 'LeaderLayer and FollowerLayer do not have a common ancestor.',
+ );
+ assert(
+ _debugCheckLeaderBeforeFollower(forwardLayers, inverseLayers),
+ 'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.',
+ );
final Matrix4 forwardTransform = _collectTransformForLayerChain(forwardLayers);
// Further transforms the coordinate system to a hypothetical child (null)
@@ -2611,7 +2548,7 @@
void addToScene(ui.SceneBuilder builder) {
assert(link != null);
assert(showWhenUnlinked != null);
- if (_leaderHandle!.leader == null && !showWhenUnlinked!) {
+ if (_link.leader == null && !showWhenUnlinked!) {
_lastTransform = null;
_lastOffset = null;
_inverseDirty = true;
diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart
index b3aee53..9e1caa3 100644
--- a/packages/flutter/lib/src/rendering/proxy_box.dart
+++ b/packages/flutter/lib/src/rendering/proxy_box.dart
@@ -5289,7 +5289,7 @@
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
// Disables the hit testing if this render object is hidden.
- if (!link.leaderConnected && !showWhenUnlinked)
+ if (link.leader == null && !showWhenUnlinked)
return false;
// RenderFollowerLayer objects don't check if they are
// themselves hit, because it's confusing to think about
@@ -5313,8 +5313,8 @@
void paint(PaintingContext context, Offset offset) {
final Size? leaderSize = link.leaderSize;
assert(
- link.leaderSize != null || (!link.leaderConnected || leaderAnchor == Alignment.topLeft),
- '$link: layer is linked to ${link.debugLeader} but a valid leaderSize is not set. '
+ link.leaderSize != null || (link.leader == null || leaderAnchor == Alignment.topLeft),
+ '$link: layer is linked to ${link.leader} but a valid leaderSize is not set. '
'leaderSize is required when leaderAnchor is not Alignment.topLeft '
'(current value is $leaderAnchor).',
);
diff --git a/packages/flutter/test/rendering/layers_test.dart b/packages/flutter/test/rendering/layers_test.dart
index cc63c23..361fb6a 100644
--- a/packages/flutter/test/rendering/layers_test.dart
+++ b/packages/flutter/test/rendering/layers_test.dart
@@ -176,7 +176,7 @@
expect(leaderLayer.link, link2);
});
- test('leader layers are always dirty when connected to follower layer', () {
+ test('leader layers not dirty when connected to follower layer', () {
final ContainerLayer root = ContainerLayer()..attach(Object());
final LayerLink link = LayerLink();
@@ -190,7 +190,7 @@
followerLayer.debugMarkClean();
leaderLayer.updateSubtreeNeedsAddToScene();
followerLayer.updateSubtreeNeedsAddToScene();
- expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
+ expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
});
test('leader layers are not dirty when all followers disconnects', () {
@@ -204,24 +204,24 @@
leaderLayer.updateSubtreeNeedsAddToScene();
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
- // Connecting a follower requires adding to scene.
+ // Connecting a follower does not require adding to scene
final FollowerLayer follower1 = FollowerLayer(link: link);
root.append(follower1);
leaderLayer.debugMarkClean();
leaderLayer.updateSubtreeNeedsAddToScene();
- expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
+ expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
final FollowerLayer follower2 = FollowerLayer(link: link);
root.append(follower2);
leaderLayer.debugMarkClean();
leaderLayer.updateSubtreeNeedsAddToScene();
- expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
+ expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
- // Disconnecting one follower, still needs add to scene.
+ // Disconnecting one follower, still does not needs add to scene.
follower2.remove();
leaderLayer.debugMarkClean();
leaderLayer.updateSubtreeNeedsAddToScene();
- expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
+ expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
// Disconnecting all followers goes back to not requiring add to scene.
follower1.remove();
diff --git a/packages/flutter/test/widgets/composited_transform_test.dart b/packages/flutter/test/widgets/composited_transform_test.dart
index 4e18b41..daaff69 100644
--- a/packages/flutter/test/widgets/composited_transform_test.dart
+++ b/packages/flutter/test/widgets/composited_transform_test.dart
@@ -300,4 +300,22 @@
}
}
});
+
+ testWidgets('Leader after Follower asserts', (WidgetTester tester) async {
+ final LayerLink link = LayerLink();
+ await tester.pumpWidget(
+ CompositedTransformFollower(
+ link: link,
+ child: CompositedTransformTarget(
+ link: link,
+ child: const SizedBox(height: 20, width: 20),
+ ),
+ ),
+ );
+
+ expect(
+ (tester.takeException() as AssertionError).message,
+ contains('LeaderLayer anchor must come before FollowerLayer in paint order'),
+ );
+ });
}