Implement paintsChild on RenderObjects that skip painting on their children (#103768)
This commit is contained in:
parent
6e7f7aea51
commit
fe87538b76
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user