Unverified Commit fe87538b authored by Dan Field's avatar Dan Field Committed by GitHub

Implement paintsChild on RenderObjects that skip painting on their children (#103768)

parent 6e7f7aea
......@@ -682,7 +682,16 @@ abstract class InkFeature {
final List<RenderObject> descendants = <RenderObject>[referenceBox];
RenderObject node = referenceBox;
while (node != _controller) {
final RenderObject childNode = node;
node = node.parent! as RenderObject;
if (!node.paintsChild(childNode)) {
// Some node between the reference box and this would skip painting on
// the reference box, so bail out early and avoid unnecessary painting.
// Some cases where this can happen are the reference box being
// offstage, in a fully transparent opacity node, or in a keep alive
// bucket.
return;
}
descendants.add(node);
}
// determine the transform that gets our coordinate system to be like theirs
......
......@@ -2701,10 +2701,35 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
///
/// Used by coordinate conversion functions to translate coordinates local to
/// one render object into coordinates local to another render object.
///
/// Some RenderObjects will provide a zeroed out matrix in this method,
/// indicating that the child should not paint anything or respond to hit
/// tests currently. A parent may supply a non-zero matrix even though it
/// does not paint its child currently, for example if the parent is a
/// [RenderOffstage] with `offstage` set to true. In both of these cases,
/// the parent must return `false` from [paintsChild].
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
assert(child.parent == this);
}
/// Whether the given child would be painted if [paint] were called.
///
/// Some RenderObjects skip painting their children if they are configured to
/// not produce any visible effects. For example, a [RenderOffstage] with
/// its `offstage` property set to true, or a [RenderOpacity] with its opacity
/// value set to zero.
///
/// In these cases, the parent may still supply a non-zero matrix in
/// [applyPaintTransform] to inform callers about where it would paint the
/// child if the child were painted at all. Alternatively, the parent may
/// supply a zeroed out matrix if it would not otherwise be able to determine
/// a valid matrix for the child and thus cannot meaningfully determine where
/// the child would paint.
bool paintsChild(covariant RenderObject child) {
assert(child.parent == this);
return true;
}
/// Applies the paint transform up the tree to `ancestor`.
///
/// Returns a matrix that maps the local paint coordinate system to the
......
......@@ -896,6 +896,12 @@ class RenderOpacity extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
@override
bool paintsChild(RenderBox child) {
assert(child.parent == this);
return _alpha > 0;
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
......@@ -1014,6 +1020,12 @@ mixin RenderAnimatedOpacityMixin<T extends RenderObject> on RenderObjectWithChil
}
}
@override
bool paintsChild(RenderObject child) {
assert(child.parent == this);
return opacity.value > 0;
}
@override
void paint(PaintingContext context, Offset offset) {
if (_alpha == 0) {
......@@ -2805,9 +2817,15 @@ class RenderFittedBox extends RenderProxyBox {
);
}
@override
bool paintsChild(RenderBox child) {
assert(child.parent == this);
return !size.isEmpty && !child.size.isEmpty;
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
if (size.isEmpty || child.size.isEmpty) {
if (!paintsChild(child)) {
transform.setZero();
} else {
_updatePaintData();
......@@ -3575,7 +3593,6 @@ class RenderOffstage extends RenderProxyBox {
return super.computeDryLayout(constraints);
}
@override
void performResize() {
assert(offstage);
......@@ -3596,6 +3613,12 @@ class RenderOffstage extends RenderProxyBox {
return !offstage && super.hitTest(result, position: position);
}
@override
bool paintsChild(RenderBox child) {
assert(child.parent == this);
return !offstage;
}
@override
void paint(PaintingContext context, Offset offset) {
if (offstage)
......
......@@ -575,19 +575,23 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
return childParentData.layoutOffset;
}
@override
bool paintsChild(RenderBox child) {
final SliverMultiBoxAdaptorParentData? childParentData = child.parentData as SliverMultiBoxAdaptorParentData?;
return childParentData?.index != null &&
!_keepAliveBucket.containsKey(childParentData!.index);
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
if (childParentData.index == null) {
// If the child has no index, such as with the prototype of a
// SliverPrototypeExtentList, then it is not visible, so we give it a
// zero transform to prevent it from painting.
transform.setZero();
} else if (_keepAliveBucket.containsKey(childParentData.index)) {
// It is possible that widgets under kept alive children want to paint
// themselves. For example, the Material widget tries to paint all
// InkFeatures under its subtree as long as they are not disposed. In
// such case, we give it a zero transform to prevent them from painting.
if (!paintsChild(child)) {
// This can happen if some child asks for the global transform even though
// they are not getting painted. In that case, the transform sets set to
// zero since [applyPaintTransformForBoxChild] would end up throwing due
// to the child not being configured correctly for applying a transform.
// There's no assert here because asking for the paint transform is a
// valid thing to do even if a child would not be painted, but there is no
// meaningful non-zero matrix to use in this case.
transform.setZero();
} else {
applyPaintTransformForBoxChild(child, transform);
......
......@@ -927,4 +927,52 @@ void main() {
);
});
});
testWidgets('InkFeature skips painting if intermediate node skips', (WidgetTester tester) async {
final GlobalKey sizedBoxKey = GlobalKey();
final GlobalKey materialKey = GlobalKey();
await tester.pumpWidget(Material(
key: materialKey,
child: Offstage(
child: SizedBox(key: sizedBoxKey, width: 20, height: 20),
),
));
final MaterialInkController controller = Material.of(sizedBoxKey.currentContext!)!;
final TrackPaintInkFeature tracker = TrackPaintInkFeature(
controller: controller,
referenceBox: sizedBoxKey.currentContext!.findRenderObject()! as RenderBox,
);
controller.addInkFeature(tracker);
expect(tracker.paintCount, 0);
// Force a repaint. Since it's offstage, the ink feture should not get painted.
materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero);
expect(tracker.paintCount, 0);
await tester.pumpWidget(Material(
key: materialKey,
child: Offstage(
offstage: false,
child: SizedBox(key: sizedBoxKey, width: 20, height: 20),
),
));
// Gets a paint because the global keys have reused the elements and it is
// now onstage.
expect(tracker.paintCount, 1);
// Force a repaint again. This time, it gets repainted because it is onstage.
materialKey.currentContext!.findRenderObject()!.paint(PaintingContext(ContainerLayer(), Rect.largest), Offset.zero);
expect(tracker.paintCount, 2);
});
}
class TrackPaintInkFeature extends InkFeature {
TrackPaintInkFeature({required super.controller, required super.referenceBox});
int paintCount = 0;
@override
void paintFeature(Canvas canvas, Matrix4 transform) {
paintCount += 1;
}
}
......@@ -687,6 +687,66 @@ void main() {
expect(() => pumpFrame(phase: EnginePhase.composite), returnsNormally);
});
test('Offstage implements paintsChild correctly', () {
final RenderBox box = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
final RenderBox parent = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
final RenderOffstage offstage = RenderOffstage(offstage: false, child: box);
parent.adoptChild(offstage);
expect(offstage.paintsChild(box), true);
offstage.offstage = true;
expect(offstage.paintsChild(box), false);
});
test('Opacity implements paintsChild correctly', () {
final RenderBox box = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
final RenderBox parent = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
final RenderOpacity opacity = RenderOpacity(child: box);
parent.adoptChild(opacity);
expect(opacity.paintsChild(box), true);
opacity.opacity = 0;
expect(opacity.paintsChild(box), false);
});
test('AnimatedOpacity sets paint matrix to zero when alpha == 0', () {
final RenderBox box = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
final RenderBox parent = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
final AnimationController opacityAnimation = AnimationController(value: 1, vsync: FakeTickerProvider());
final RenderAnimatedOpacity opacity = RenderAnimatedOpacity(opacity: opacityAnimation, child: box);
parent.adoptChild(opacity);
// Make it listen to the animation.
opacity.attach(PipelineOwner());
expect(opacity.paintsChild(box), true);
opacityAnimation.value = 0;
expect(opacity.paintsChild(box), false);
});
test('AnimatedOpacity sets paint matrix to zero when alpha == 0 (sliver)', () {
final RenderSliver sliver = RenderSliverToBoxAdapter(child: RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20)));
final RenderBox parent = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
final AnimationController opacityAnimation = AnimationController(value: 1, vsync: FakeTickerProvider());
final RenderSliverAnimatedOpacity opacity = RenderSliverAnimatedOpacity(opacity: opacityAnimation, sliver: sliver);
parent.adoptChild(opacity);
// Make it listen to the animation.
opacity.attach(PipelineOwner());
expect(opacity.paintsChild(sliver), true);
opacityAnimation.value = 0;
expect(opacity.paintsChild(sliver), false);
});
test('RenderCustomClip extenders respect clipBehavior when asked to describeApproximateClip', () {
final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200));
final RenderClipRect renderClipRect = RenderClipRect(clipBehavior: Clip.none, child: child);
......
......@@ -110,6 +110,44 @@ void main() {
expect(actual, 0);
});
});
test('Implements paintsChild correctly', () {
final List<RenderBox> children = <RenderBox>[
RenderSizedBox(const Size(400.0, 100.0)),
RenderSizedBox(const Size(400.0, 100.0)),
RenderSizedBox(const Size(400.0, 100.0)),
];
final TestRenderSliverBoxChildManager childManager = TestRenderSliverBoxChildManager(
children: children,
);
final RenderViewport root = RenderViewport(
crossAxisDirection: AxisDirection.right,
offset: ViewportOffset.zero(),
cacheExtent: 0,
children: <RenderSliver>[
childManager.createRenderSliverFillViewport(),
],
);
layout(root);
expect(children.first.parent, isA<RenderSliverMultiBoxAdaptor>());
final RenderSliverMultiBoxAdaptor parent = children.first.parent! as RenderSliverMultiBoxAdaptor;
expect(parent.paintsChild(children[0]), true);
expect(parent.paintsChild(children[1]), false);
expect(parent.paintsChild(children[2]), false);
root.offset = ViewportOffset.fixed(600);
pumpFrame();
expect(parent.paintsChild(children[0]), false);
expect(parent.paintsChild(children[1]), true);
expect(parent.paintsChild(children[2]), false);
root.offset = ViewportOffset.fixed(1200);
pumpFrame();
expect(parent.paintsChild(children[0]), false);
expect(parent.paintsChild(children[1]), false);
expect(parent.paintsChild(children[2]), true);
});
}
int testGetMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment