Skip to content

Commit fe87538

Browse files
authored
Implement paintsChild on RenderObjects that skip painting on their children (#103768)
1 parent 6e7f7ae commit fe87538

File tree

7 files changed

+220
-13
lines changed

7 files changed

+220
-13
lines changed

packages/flutter/lib/src/material/material.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,16 @@ abstract class InkFeature {
682682
final List<RenderObject> descendants = <RenderObject>[referenceBox];
683683
RenderObject node = referenceBox;
684684
while (node != _controller) {
685+
final RenderObject childNode = node;
685686
node = node.parent! as RenderObject;
687+
if (!node.paintsChild(childNode)) {
688+
// Some node between the reference box and this would skip painting on
689+
// the reference box, so bail out early and avoid unnecessary painting.
690+
// Some cases where this can happen are the reference box being
691+
// offstage, in a fully transparent opacity node, or in a keep alive
692+
// bucket.
693+
return;
694+
}
686695
descendants.add(node);
687696
}
688697
// determine the transform that gets our coordinate system to be like theirs

packages/flutter/lib/src/rendering/object.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2701,10 +2701,35 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
27012701
///
27022702
/// Used by coordinate conversion functions to translate coordinates local to
27032703
/// one render object into coordinates local to another render object.
2704+
///
2705+
/// Some RenderObjects will provide a zeroed out matrix in this method,
2706+
/// indicating that the child should not paint anything or respond to hit
2707+
/// tests currently. A parent may supply a non-zero matrix even though it
2708+
/// does not paint its child currently, for example if the parent is a
2709+
/// [RenderOffstage] with `offstage` set to true. In both of these cases,
2710+
/// the parent must return `false` from [paintsChild].
27042711
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
27052712
assert(child.parent == this);
27062713
}
27072714

2715+
/// Whether the given child would be painted if [paint] were called.
2716+
///
2717+
/// Some RenderObjects skip painting their children if they are configured to
2718+
/// not produce any visible effects. For example, a [RenderOffstage] with
2719+
/// its `offstage` property set to true, or a [RenderOpacity] with its opacity
2720+
/// value set to zero.
2721+
///
2722+
/// In these cases, the parent may still supply a non-zero matrix in
2723+
/// [applyPaintTransform] to inform callers about where it would paint the
2724+
/// child if the child were painted at all. Alternatively, the parent may
2725+
/// supply a zeroed out matrix if it would not otherwise be able to determine
2726+
/// a valid matrix for the child and thus cannot meaningfully determine where
2727+
/// the child would paint.
2728+
bool paintsChild(covariant RenderObject child) {
2729+
assert(child.parent == this);
2730+
return true;
2731+
}
2732+
27082733
/// Applies the paint transform up the tree to `ancestor`.
27092734
///
27102735
/// Returns a matrix that maps the local paint coordinate system to the

packages/flutter/lib/src/rendering/proxy_box.dart

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,12 @@ class RenderOpacity extends RenderProxyBox {
896896
markNeedsSemanticsUpdate();
897897
}
898898

899+
@override
900+
bool paintsChild(RenderBox child) {
901+
assert(child.parent == this);
902+
return _alpha > 0;
903+
}
904+
899905
@override
900906
void paint(PaintingContext context, Offset offset) {
901907
if (child != null) {
@@ -1014,6 +1020,12 @@ mixin RenderAnimatedOpacityMixin<T extends RenderObject> on RenderObjectWithChil
10141020
}
10151021
}
10161022

1023+
@override
1024+
bool paintsChild(RenderObject child) {
1025+
assert(child.parent == this);
1026+
return opacity.value > 0;
1027+
}
1028+
10171029
@override
10181030
void paint(PaintingContext context, Offset offset) {
10191031
if (_alpha == 0) {
@@ -2805,9 +2817,15 @@ class RenderFittedBox extends RenderProxyBox {
28052817
);
28062818
}
28072819

2820+
@override
2821+
bool paintsChild(RenderBox child) {
2822+
assert(child.parent == this);
2823+
return !size.isEmpty && !child.size.isEmpty;
2824+
}
2825+
28082826
@override
28092827
void applyPaintTransform(RenderBox child, Matrix4 transform) {
2810-
if (size.isEmpty || child.size.isEmpty) {
2828+
if (!paintsChild(child)) {
28112829
transform.setZero();
28122830
} else {
28132831
_updatePaintData();
@@ -3575,7 +3593,6 @@ class RenderOffstage extends RenderProxyBox {
35753593
return super.computeDryLayout(constraints);
35763594
}
35773595

3578-
35793596
@override
35803597
void performResize() {
35813598
assert(offstage);
@@ -3596,6 +3613,12 @@ class RenderOffstage extends RenderProxyBox {
35963613
return !offstage && super.hitTest(result, position: position);
35973614
}
35983615

3616+
@override
3617+
bool paintsChild(RenderBox child) {
3618+
assert(child.parent == this);
3619+
return !offstage;
3620+
}
3621+
35993622
@override
36003623
void paint(PaintingContext context, Offset offset) {
36013624
if (offstage)

packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -575,19 +575,23 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
575575
return childParentData.layoutOffset;
576576
}
577577

578+
@override
579+
bool paintsChild(RenderBox child) {
580+
final SliverMultiBoxAdaptorParentData? childParentData = child.parentData as SliverMultiBoxAdaptorParentData?;
581+
return childParentData?.index != null &&
582+
!_keepAliveBucket.containsKey(childParentData!.index);
583+
}
584+
578585
@override
579586
void applyPaintTransform(RenderBox child, Matrix4 transform) {
580-
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
581-
if (childParentData.index == null) {
582-
// If the child has no index, such as with the prototype of a
583-
// SliverPrototypeExtentList, then it is not visible, so we give it a
584-
// zero transform to prevent it from painting.
585-
transform.setZero();
586-
} else if (_keepAliveBucket.containsKey(childParentData.index)) {
587-
// It is possible that widgets under kept alive children want to paint
588-
// themselves. For example, the Material widget tries to paint all
589-
// InkFeatures under its subtree as long as they are not disposed. In
590-
// such case, we give it a zero transform to prevent them from painting.
587+
if (!paintsChild(child)) {
588+
// This can happen if some child asks for the global transform even though
589+
// they are not getting painted. In that case, the transform sets set to
590+
// zero since [applyPaintTransformForBoxChild] would end up throwing due
591+
// to the child not being configured correctly for applying a transform.
592+
// There's no assert here because asking for the paint transform is a
593+
// valid thing to do even if a child would not be painted, but there is no
594+
// meaningful non-zero matrix to use in this case.
591595
transform.setZero();
592596
} else {
593597
applyPaintTransformForBoxChild(child, transform);

packages/flutter/test/material/material_test.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,4 +927,52 @@ void main() {
927927
);
928928
});
929929
});
930+
931+
testWidgets('InkFeature skips painting if intermediate node skips', (WidgetTester tester) async {
932+
final GlobalKey sizedBoxKey = GlobalKey();
933+
final GlobalKey materialKey = GlobalKey();
934+
await tester.pumpWidget(Material(
935+
key: materialKey,
936+
child: Offstage(
937+
child: SizedBox(key: sizedBoxKey, width: 20, height: 20),
938+
),
939+
));
940+
final MaterialInkController controller = Material.of(sizedBoxKey.currentContext!)!;
941+
942+
final TrackPaintInkFeature tracker = TrackPaintInkFeature(
943+
controller: controller,
944+
referenceBox: sizedBoxKey.currentContext!.findRenderObject()! as RenderBox,
945+
);
946+
controller.addInkFeature(tracker);
947+
expect(tracker.paintCount, 0);
948+
949+
// Force a repaint. Since it's offstage, the ink feture should not get painted.
950+
materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero);
951+
expect(tracker.paintCount, 0);
952+
953+
await tester.pumpWidget(Material(
954+
key: materialKey,
955+
child: Offstage(
956+
offstage: false,
957+
child: SizedBox(key: sizedBoxKey, width: 20, height: 20),
958+
),
959+
));
960+
// Gets a paint because the global keys have reused the elements and it is
961+
// now onstage.
962+
expect(tracker.paintCount, 1);
963+
964+
// Force a repaint again. This time, it gets repainted because it is onstage.
965+
materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero);
966+
expect(tracker.paintCount, 2);
967+
});
968+
}
969+
970+
class TrackPaintInkFeature extends InkFeature {
971+
TrackPaintInkFeature({required super.controller, required super.referenceBox});
972+
973+
int paintCount = 0;
974+
@override
975+
void paintFeature(Canvas canvas, Matrix4 transform) {
976+
paintCount += 1;
977+
}
930978
}

packages/flutter/test/rendering/proxy_box_test.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,66 @@ void main() {
687687
expect(() => pumpFrame(phase: EnginePhase.composite), returnsNormally);
688688
});
689689

690+
test('Offstage implements paintsChild correctly', () {
691+
final RenderBox box = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
692+
final RenderBox parent = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
693+
final RenderOffstage offstage = RenderOffstage(offstage: false, child: box);
694+
parent.adoptChild(offstage);
695+
696+
expect(offstage.paintsChild(box), true);
697+
698+
offstage.offstage = true;
699+
700+
expect(offstage.paintsChild(box), false);
701+
});
702+
703+
test('Opacity implements paintsChild correctly', () {
704+
final RenderBox box = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
705+
final RenderBox parent = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
706+
final RenderOpacity opacity = RenderOpacity(child: box);
707+
parent.adoptChild(opacity);
708+
709+
expect(opacity.paintsChild(box), true);
710+
711+
opacity.opacity = 0;
712+
713+
expect(opacity.paintsChild(box), false);
714+
});
715+
716+
test('AnimatedOpacity sets paint matrix to zero when alpha == 0', () {
717+
final RenderBox box = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
718+
final RenderBox parent = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
719+
final AnimationController opacityAnimation = AnimationController(value: 1, vsync: FakeTickerProvider());
720+
final RenderAnimatedOpacity opacity = RenderAnimatedOpacity(opacity: opacityAnimation, child: box);
721+
parent.adoptChild(opacity);
722+
723+
// Make it listen to the animation.
724+
opacity.attach(PipelineOwner());
725+
726+
expect(opacity.paintsChild(box), true);
727+
728+
opacityAnimation.value = 0;
729+
730+
expect(opacity.paintsChild(box), false);
731+
});
732+
733+
test('AnimatedOpacity sets paint matrix to zero when alpha == 0 (sliver)', () {
734+
final RenderSliver sliver = RenderSliverToBoxAdapter(child: RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20)));
735+
final RenderBox parent = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
736+
final AnimationController opacityAnimation = AnimationController(value: 1, vsync: FakeTickerProvider());
737+
final RenderSliverAnimatedOpacity opacity = RenderSliverAnimatedOpacity(opacity: opacityAnimation, sliver: sliver);
738+
parent.adoptChild(opacity);
739+
740+
// Make it listen to the animation.
741+
opacity.attach(PipelineOwner());
742+
743+
expect(opacity.paintsChild(sliver), true);
744+
745+
opacityAnimation.value = 0;
746+
747+
expect(opacity.paintsChild(sliver), false);
748+
});
749+
690750
test('RenderCustomClip extenders respect clipBehavior when asked to describeApproximateClip', () {
691751
final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200));
692752
final RenderClipRect renderClipRect = RenderClipRect(clipBehavior: Clip.none, child: child);

packages/flutter/test/rendering/sliver_fixed_extent_layout_test.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,44 @@ void main() {
110110
expect(actual, 0);
111111
});
112112
});
113+
114+
test('Implements paintsChild correctly', () {
115+
final List<RenderBox> children = <RenderBox>[
116+
RenderSizedBox(const Size(400.0, 100.0)),
117+
RenderSizedBox(const Size(400.0, 100.0)),
118+
RenderSizedBox(const Size(400.0, 100.0)),
119+
];
120+
final TestRenderSliverBoxChildManager childManager = TestRenderSliverBoxChildManager(
121+
children: children,
122+
);
123+
final RenderViewport root = RenderViewport(
124+
crossAxisDirection: AxisDirection.right,
125+
offset: ViewportOffset.zero(),
126+
cacheExtent: 0,
127+
children: <RenderSliver>[
128+
childManager.createRenderSliverFillViewport(),
129+
],
130+
);
131+
layout(root);
132+
expect(children.first.parent, isA<RenderSliverMultiBoxAdaptor>());
133+
134+
final RenderSliverMultiBoxAdaptor parent = children.first.parent! as RenderSliverMultiBoxAdaptor;
135+
expect(parent.paintsChild(children[0]), true);
136+
expect(parent.paintsChild(children[1]), false);
137+
expect(parent.paintsChild(children[2]), false);
138+
139+
root.offset = ViewportOffset.fixed(600);
140+
pumpFrame();
141+
expect(parent.paintsChild(children[0]), false);
142+
expect(parent.paintsChild(children[1]), true);
143+
expect(parent.paintsChild(children[2]), false);
144+
145+
root.offset = ViewportOffset.fixed(1200);
146+
pumpFrame();
147+
expect(parent.paintsChild(children[0]), false);
148+
expect(parent.paintsChild(children[1]), false);
149+
expect(parent.paintsChild(children[2]), true);
150+
});
113151
}
114152

115153
int testGetMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) {

0 commit comments

Comments
 (0)