Do not crash on LeaderLayer.applyTransform after retained rendering (#96144)
diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart
index 868bd5a..ddf5e3a 100644
--- a/packages/flutter/lib/src/rendering/layer.dart
+++ b/packages/flutter/lib/src/rendering/layer.dart
@@ -2234,7 +2234,7 @@
void attach(Object owner) {
super.attach(owner);
assert(link._leader == null);
- _lastOffset = null;
+ assert(_debugSetLastOffset(null));
link._leader = this;
}
@@ -2242,7 +2242,7 @@
void detach() {
assert(link._leader == this);
link._leader = null;
- _lastOffset = null;
+ assert(_debugSetLastOffset(null));
super.detach();
}
@@ -2251,7 +2251,17 @@
/// 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? _lastOffset;
+ 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 }) {
@@ -2261,14 +2271,14 @@
@override
void addToScene(ui.SceneBuilder builder) {
assert(offset != null);
- _lastOffset = offset;
- if (_lastOffset != Offset.zero)
+ assert(_debugSetLastOffset(offset));
+ if (offset != Offset.zero)
engineLayer = builder.pushTransform(
- Matrix4.translationValues(_lastOffset!.dx, _lastOffset!.dy, 0.0).storage,
+ Matrix4.translationValues(offset.dx, offset.dy, 0.0).storage,
oldLayer: _engineLayer as ui.TransformEngineLayer?,
);
addChildrenToScene(builder);
- if (_lastOffset != Offset.zero)
+ if (offset != Offset.zero)
builder.pop();
}
@@ -2281,9 +2291,8 @@
/// children.
@override
void applyTransform(Layer? child, Matrix4 transform) {
- assert(_lastOffset != null);
- if (_lastOffset != Offset.zero)
- transform.translate(_lastOffset!.dx, _lastOffset!.dy);
+ if (offset != Offset.zero)
+ transform.translate(offset.dx, offset.dy);
}
@override
@@ -2499,7 +2508,7 @@
'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.',
);
assert(
- leader._lastOffset != null,
+ leader._debugLastOffset != null,
'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.',
);
diff --git a/packages/flutter/test/rendering/layers_test.dart b/packages/flutter/test/rendering/layers_test.dart
index 1d9aa4a..8f477a8 100644
--- a/packages/flutter/test/rendering/layers_test.dart
+++ b/packages/flutter/test/rendering/layers_test.dart
@@ -218,6 +218,36 @@
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
});
+ test('LeaderLayer.applyTransform can be called after retained rendering', () {
+ void expectTransform(RenderObject leader) {
+ final LeaderLayer leaderLayer = leader.debugLayer! as LeaderLayer;
+ final Matrix4 expected = Matrix4.identity()
+ ..translate(leaderLayer.offset.dx, leaderLayer.offset.dy);
+ final Matrix4 transformed = Matrix4.identity();
+ leaderLayer.applyTransform(null, transformed);
+ expect(transformed, expected);
+ }
+
+ final LayerLink link = LayerLink();
+ late RenderLeaderLayer leader;
+ final RenderRepaintBoundary root = RenderRepaintBoundary(
+ child:RenderRepaintBoundary(
+ child: leader = RenderLeaderLayer(link: link),
+ ),
+ );
+ layout(root, phase: EnginePhase.composite);
+
+ expectTransform(leader);
+
+ // Causes a repaint, but the LeaderLayer of RenderLeaderLayer will be added
+ // as retained and LeaderLayer.addChildrenToScene will not be called.
+ root.markNeedsPaint();
+ pumpFrame(phase: EnginePhase.composite);
+
+ // The LeaderLayer.applyTransform call shouldn't crash.
+ expectTransform(leader);
+ });
+
test('depthFirstIterateChildren', () {
final ContainerLayer a = ContainerLayer();
final ContainerLayer b = ContainerLayer();