Skip to content
This repository was archived by the owner on Oct 13, 2025. It is now read-only.

Commit 3896dc7

Browse files
authored
Use the layer tree to do more efficient calculation of transform/clip (#401)
1 parent 77e29f1 commit 3896dc7

File tree

3 files changed

+114
-22
lines changed

3 files changed

+114
-22
lines changed

packages/visibility_detector/lib/src/render_visibility_detector.dart

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ mixin RenderVisibilityDetectorBase on RenderObject {
165165
}
166166

167167
VisibilityInfo _determineVisibility(ContainerLayer? layer, Rect bounds) {
168-
if (_disposed || layer?.attached == false || !attached) {
168+
if (_disposed || layer == null || layer.attached == false || !attached) {
169169
// layer is detached and thus invisible.
170170
return VisibilityInfo(
171171
key: key,
@@ -174,39 +174,50 @@ mixin RenderVisibilityDetectorBase on RenderObject {
174174
}
175175
final transform = Matrix4.identity();
176176

177-
// Create a list of RenderObjects from this to the root, excluding the root
177+
// Check if any ancestors decided to skip painting this RenderObject.
178+
if (parent != null) {
179+
RenderObject ancestor = parent! as RenderObject;
180+
RenderObject child = this;
181+
while (ancestor.parent != null) {
182+
if (!ancestor.paintsChild(child)) {
183+
return VisibilityInfo(key: key, size: bounds.size);
184+
}
185+
child = ancestor;
186+
ancestor = ancestor.parent! as RenderObject;
187+
}
188+
}
189+
190+
// Create a list of Layers from layer to the root, excluding the root
178191
// since that has the DPR transform and we want to work with logical pixels.
179-
// Cannot use the layer tree since some ancestor render object may have
180-
// directly transformed/clipped the canvas. If there is some way to figure
181-
// out how to get the RenderObjects below [layer], could take advantage of
182-
// the usually shallower height of the layer tree compared to the render
183-
// tree. Alternatively, if the canvas itself exposed the current matrix/clip
184-
// we could use that.
185-
RenderObject? ancestor = parent as RenderObject?;
186-
187-
final List<RenderObject> ancestors = <RenderObject>[];
188-
ancestors.add(this);
189-
RenderObject child = this;
192+
// Add one extra leaf layer so that we can apply the transform of `layer`
193+
// to the matrix.
194+
ContainerLayer? ancestor = layer;
195+
final List<ContainerLayer> ancestors = <ContainerLayer>[ContainerLayer()];
190196
while (ancestor != null && ancestor.parent != null) {
191-
if (!ancestor.paintsChild(child)) {
192-
return VisibilityInfo(key: key, size: bounds.size);
193-
}
194197
ancestors.add(ancestor);
195-
child = ancestor;
196-
ancestor = ancestor.parent as RenderObject?;
198+
ancestor = ancestor.parent;
197199
}
198200

199-
// Determine the transform and clip from first child of root down to
200-
// this.
201201
Rect clip = Rect.largest;
202202
for (int index = ancestors.length - 1; index > 0; index -= 1) {
203203
final parent = ancestors[index];
204204
final child = ancestors[index - 1];
205-
Rect? parentClip = parent.describeApproximatePaintClip(child);
205+
Rect? parentClip = parent.describeClipBounds();
206206
if (parentClip != null) {
207207
clip = clip.intersect(MatrixUtils.transformRect(transform, parentClip));
208208
}
209-
parent.applyPaintTransform(child, transform);
209+
parent.applyTransform(child, transform);
210+
}
211+
212+
// Apply whatever transform/clip was on the canvas when painting.
213+
if (_lastPaintClipBounds != null) {
214+
clip = clip.intersect(MatrixUtils.transformRect(
215+
transform,
216+
_lastPaintClipBounds!,
217+
));
218+
}
219+
if (_lastPaintTransform != null) {
220+
transform.multiply(_lastPaintTransform!);
210221
}
211222
return VisibilityInfo.fromRects(
212223
key: key,
@@ -219,9 +230,17 @@ mixin RenderVisibilityDetectorBase on RenderObject {
219230
/// clients about visibility.
220231
Rect get bounds;
221232

233+
Matrix4? _lastPaintTransform;
234+
Rect? _lastPaintClipBounds;
235+
222236
@override
223237
void paint(PaintingContext context, Offset offset) {
224238
if (onVisibilityChanged != null) {
239+
_lastPaintClipBounds = context.canvas.getLocalClipBounds();
240+
_lastPaintTransform =
241+
Matrix4.fromFloat64List(context.canvas.getTransform())
242+
..translate(offset.dx, offset.dy, 0);
243+
225244
_compositionCallbackCanceller?.call();
226245
_compositionCallbackCanceller =
227246
context.addCompositionCallback((Layer layer) {

packages/visibility_detector/test/render_visibility_detector_test.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ void main() {
2828
detector.layout(BoxConstraints.tight(const Size(200, 200)));
2929
detector.paint(context, Offset.zero);
3030
detector.paint(context, Offset.zero);
31+
32+
context.stopRecordingIfNeeded(); // ignore: invalid_use_of_protected_member
33+
3134
expect(layer.subtreeHasCompositionCallbacks, true);
3235

3336
expect(detector.debugScheduleUpdateCount, 0);
@@ -73,6 +76,7 @@ void main() {
7376
expect(layer.subtreeHasCompositionCallbacks, true);
7477

7578
expect(detector.debugScheduleUpdateCount, 0);
79+
context.stopRecordingIfNeeded(); // ignore: invalid_use_of_protected_member
7680
layer.buildScene(SceneBuilder()).dispose();
7781

7882
expect(detector.debugScheduleUpdateCount, 1);
@@ -98,6 +102,7 @@ void main() {
98102
expect(layer.subtreeHasCompositionCallbacks, false);
99103

100104
expect(detector.debugScheduleUpdateCount, 0);
105+
context.stopRecordingIfNeeded(); // ignore: invalid_use_of_protected_member
101106
layer.buildScene(SceneBuilder()).dispose();
102107

103108
expect(detector.debugScheduleUpdateCount, 0);
@@ -124,6 +129,7 @@ void main() {
124129
expect(layer.subtreeHasCompositionCallbacks, false);
125130

126131
expect(detector.debugScheduleUpdateCount, 0);
132+
context.stopRecordingIfNeeded(); // ignore: invalid_use_of_protected_member
127133
layer.buildScene(SceneBuilder()).dispose();
128134

129135
expect(detector.debugScheduleUpdateCount, 0);

packages/visibility_detector/test/widget_test.dart

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,73 @@ void main() {
2525

2626
tearDown(_positionToVisibilityInfo.clear);
2727

28+
testWidgets('Offset coming into paint applied to clip',
29+
(WidgetTester tester) async {
30+
VisibilityInfo? lastInfo;
31+
await tester.pumpWidget(
32+
Center(
33+
child: SizedBox(
34+
height: 400,
35+
width: 400,
36+
child: Transform(
37+
transform: Matrix4.identity()..translate(0.0, -300.0),
38+
child: ClipRect(
39+
clipBehavior: Clip.hardEdge,
40+
child: VisibilityDetector(
41+
key: UniqueKey(),
42+
onVisibilityChanged: (info) {
43+
lastInfo = info;
44+
},
45+
child: Container(width: 200, height: 350, color: Colors.red),
46+
),
47+
),
48+
),
49+
),
50+
),
51+
);
52+
await tester.pump(VisibilityDetectorController.instance.updateInterval);
53+
expect(lastInfo, isNotNull);
54+
expect(lastInfo!.visibleFraction, 1.0);
55+
await tester.pumpWidget(const Placeholder());
56+
await tester.pump(VisibilityDetectorController.instance.updateInterval);
57+
});
58+
59+
testWidgets('Transform in layer tree, clip on canvas gets transformed',
60+
(WidgetTester tester) async {
61+
VisibilityInfo? lastInfo;
62+
await tester.pumpWidget(
63+
Center(
64+
child: SizedBox(
65+
height: 400,
66+
width: 400,
67+
child: Transform(
68+
transform: Matrix4.identity()
69+
..translate(0.0, -300.0)
70+
..scale(-.9, -.9),
71+
child: Opacity(
72+
opacity: .8,
73+
child: ClipRect(
74+
clipBehavior: Clip.hardEdge,
75+
child: VisibilityDetector(
76+
key: UniqueKey(),
77+
onVisibilityChanged: (info) {
78+
lastInfo = info;
79+
},
80+
child: Container(width: 200, height: 350, color: Colors.red),
81+
),
82+
),
83+
),
84+
),
85+
),
86+
),
87+
);
88+
await tester.pump(VisibilityDetectorController.instance.updateInterval);
89+
expect(lastInfo, isNotNull);
90+
expect(lastInfo!.visibleFraction, 1.0);
91+
await tester.pumpWidget(const Placeholder());
92+
await tester.pump(VisibilityDetectorController.instance.updateInterval);
93+
});
94+
2895
_wrapTest(
2996
'VisibilityDetector properly builds',
3097
callback: (tester) async {

0 commit comments

Comments
 (0)