Skip to content

Commit 0cb5772

Browse files
[framework] allow other RenderObjects to behave like repaint boundaries (#101952)
1 parent 61bbaaa commit 0cb5772

File tree

7 files changed

+794
-58
lines changed

7 files changed

+794
-58
lines changed

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

Lines changed: 187 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -121,22 +121,38 @@ class PaintingContext extends ClipContext {
121121
if (childLayer == null) {
122122
assert(debugAlsoPaintedParent);
123123
assert(child._layerHandle.layer == null);
124+
124125
// Not using the `layer` setter because the setter asserts that we not
125126
// replace the layer for repaint boundaries. That assertion does not
126127
// apply here because this is exactly the place designed to create a
127128
// layer for repaint boundaries.
128-
final OffsetLayer layer = OffsetLayer();
129+
final OffsetLayer layer = child.updateCompositedLayer(oldLayer: null);
129130
child._layerHandle.layer = childLayer = layer;
130131
} else {
131132
assert(debugAlsoPaintedParent || childLayer.attached);
133+
Offset? debugOldOffset;
134+
assert(() {
135+
debugOldOffset = childLayer!.offset;
136+
return true;
137+
}());
132138
childLayer.removeAllChildren();
139+
final OffsetLayer updatedLayer = child.updateCompositedLayer(oldLayer: childLayer);
140+
assert(identical(updatedLayer, childLayer),
141+
'$child created a new layer instance $updatedLayer instead of reusing the '
142+
'existing layer $childLayer. See the documentation of RenderObject.updateCompositedLayer '
143+
'for more information on how to correctly implement this method.'
144+
);
145+
assert(debugOldOffset == updatedLayer.offset);
133146
}
147+
child._needsCompositedLayerUpdate = false;
148+
134149
assert(identical(childLayer, child._layerHandle.layer));
135150
assert(child._layerHandle.layer is OffsetLayer);
136151
assert(() {
137152
childLayer!.debugCreator = child.debugCreator ?? child.runtimeType;
138153
return true;
139154
}());
155+
140156
childContext ??= PaintingContext(childLayer, child.paintBounds);
141157
child._paintWithContext(childContext, Offset.zero);
142158

@@ -146,6 +162,38 @@ class PaintingContext extends ClipContext {
146162
childContext.stopRecordingIfNeeded();
147163
}
148164

165+
/// Update the composited layer of [child] without repainting its children.
166+
///
167+
/// The render object must be attached to a [PipelineOwner], must have a
168+
/// composited layer, and must be in need of a composited layer update but
169+
/// not in need of painting. The render object's layer is re-used, and none
170+
/// of its children are repaint or their layers updated.
171+
///
172+
/// See also:
173+
///
174+
/// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject]
175+
/// has a composited layer.
176+
static void updateLayerProperties(RenderObject child) {
177+
assert(child.isRepaintBoundary && child._wasRepaintBoundary);
178+
assert(!child._needsPaint);
179+
assert(child._layerHandle.layer != null);
180+
181+
final OffsetLayer childLayer = child._layerHandle.layer! as OffsetLayer;
182+
Offset? debugOldOffset;
183+
assert(() {
184+
debugOldOffset = childLayer.offset;
185+
return true;
186+
}());
187+
final OffsetLayer updatedLayer = child.updateCompositedLayer(oldLayer: childLayer);
188+
assert(identical(updatedLayer, childLayer),
189+
'$child created a new layer instance $updatedLayer instead of reusing the '
190+
'existing layer $childLayer. See the documentation of RenderObject.updateCompositedLayer '
191+
'for more information on how to correctly implement this method.'
192+
);
193+
assert(debugOldOffset == updatedLayer.offset);
194+
child._needsCompositedLayerUpdate = false;
195+
}
196+
149197
/// In debug mode, repaint the given render object using a custom painting
150198
/// context that can record the results of the painting operation in addition
151199
/// to performing the regular paint of the child.
@@ -183,6 +231,12 @@ class PaintingContext extends ClipContext {
183231
if (child.isRepaintBoundary) {
184232
stopRecordingIfNeeded();
185233
_compositeChild(child, offset);
234+
// If a render object was a repaint boundary but no longer is one, this
235+
// is where the framework managed layer is automatically disposed.
236+
} else if (child._wasRepaintBoundary) {
237+
assert(child._layerHandle.layer is OffsetLayer);
238+
child._layerHandle.layer = null;
239+
child._paintWithContext(this, offset);
186240
} else {
187241
child._paintWithContext(this, offset);
188242
}
@@ -194,9 +248,12 @@ class PaintingContext extends ClipContext {
194248
assert(_canvas == null || _canvas!.getSaveCount() == 1);
195249

196250
// Create a layer for our child, and paint the child into it.
197-
if (child._needsPaint) {
251+
if (child._needsPaint || !child._wasRepaintBoundary) {
198252
repaintCompositedChild(child, debugAlsoPaintedParent: true);
199253
} else {
254+
if (child._needsCompositedLayerUpdate) {
255+
updateLayerProperties(child);
256+
}
200257
assert(() {
201258
// register the call for RepaintBoundary metrics
202259
child.debugRegisterRepaintBoundaryPaint();
@@ -978,19 +1035,25 @@ class PipelineOwner {
9781035
arguments: debugTimelineArguments,
9791036
);
9801037
}
981-
assert(() {
982-
_debugDoingPaint = true;
983-
return true;
984-
}());
9851038
try {
1039+
assert(() {
1040+
_debugDoingPaint = true;
1041+
return true;
1042+
}());
9861043
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
9871044
_nodesNeedingPaint = <RenderObject>[];
1045+
9881046
// Sort the dirty nodes in reverse order (deepest first).
9891047
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
9901048
assert(node._layerHandle.layer != null);
991-
if (node._needsPaint && node.owner == this) {
1049+
if ((node._needsPaint || node._needsCompositedLayerUpdate) && node.owner == this) {
9921050
if (node._layerHandle.layer!.attached) {
993-
PaintingContext.repaintCompositedChild(node);
1051+
assert(node.isRepaintBoundary);
1052+
if (node._needsPaint) {
1053+
PaintingContext.repaintCompositedChild(node);
1054+
} else {
1055+
PaintingContext.updateLayerProperties(node);
1056+
}
9941057
} else {
9951058
node._skippedPaintingOnLayer();
9961059
}
@@ -1236,6 +1299,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
12361299
/// Initializes internal fields for subclasses.
12371300
RenderObject() {
12381301
_needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
1302+
_wasRepaintBoundary = isRepaintBoundary;
12391303
}
12401304

12411305
/// Cause the entire subtree rooted at the given [RenderObject] to be marked
@@ -2070,12 +2134,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
20702134
/// to repaint.
20712135
///
20722136
/// If this getter returns true, the [paintBounds] are applied to this object
2073-
/// and all descendants. The framework automatically creates an [OffsetLayer]
2074-
/// and assigns it to the [layer] field. Render objects that declare
2075-
/// themselves as repaint boundaries must not replace the layer created by
2076-
/// the framework.
2137+
/// and all descendants. The framework invokes [RenderObject.updateCompositedLayer]
2138+
/// to create an [OffsetLayer] and assigns it to the [layer] field.
2139+
/// Render objects that declare themselves as repaint boundaries must not replace
2140+
/// the layer created by the framework.
20772141
///
2078-
/// Warning: This getter must not change value over the lifetime of this object.
2142+
/// If the value of this getter changes, [markNeedsCompositingBitsUpdate] must
2143+
/// be called.
20792144
///
20802145
/// See [RepaintBoundary] for more information about how repaint boundaries function.
20812146
bool get isRepaintBoundary => false;
@@ -2098,6 +2163,34 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
20982163
@protected
20992164
bool get alwaysNeedsCompositing => false;
21002165

2166+
late bool _wasRepaintBoundary;
2167+
2168+
/// Update the composited layer owned by this render object.
2169+
///
2170+
/// This method is called by the framework when [isRepaintBoundary] is true.
2171+
///
2172+
/// If [oldLayer] is `null`, this method must return a new [OffsetLayer]
2173+
/// (or subtype thereof). If [oldLayer] is not `null`, then this method must
2174+
/// reuse the layer instance that is provided - it is an error to create a new
2175+
/// layer in this instance. The layer will be disposed by the framework when
2176+
/// either the render object is disposed or if it is no longer a repaint
2177+
/// boundary.
2178+
///
2179+
/// The [OffsetLayer.offset] property will be managed by the framework and
2180+
/// must not be updated by this method.
2181+
///
2182+
/// If a property of the composited layer needs to be updated, the render object
2183+
/// must call [markNeedsCompositedLayerUpdate] which will schedule this method
2184+
/// to be called without repainting children. If this widget was marked as
2185+
/// needing to paint and needing a composited layer update, this method is only
2186+
/// called once.
2187+
// TODO(jonahwilliams): https://github.com/flutter/flutter/issues/102102 revisit the
2188+
// contraint that the instance/type of layer cannot be changed at runtime.
2189+
OffsetLayer updateCompositedLayer({required covariant OffsetLayer? oldLayer}) {
2190+
assert(isRepaintBoundary);
2191+
return oldLayer ?? OffsetLayer();
2192+
}
2193+
21012194
/// The compositing layer that this render object uses to repaint.
21022195
///
21032196
/// If this render object is not a repaint boundary, it is the responsibility
@@ -2184,7 +2277,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
21842277
final RenderObject parent = this.parent! as RenderObject;
21852278
if (parent._needsCompositingBitsUpdate)
21862279
return;
2187-
if (!isRepaintBoundary && !parent.isRepaintBoundary) {
2280+
2281+
if ((!_wasRepaintBoundary || !isRepaintBoundary) && !parent.isRepaintBoundary) {
21882282
parent.markNeedsCompositingBitsUpdate();
21892283
return;
21902284
}
@@ -2225,9 +2319,23 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
22252319
});
22262320
if (isRepaintBoundary || alwaysNeedsCompositing)
22272321
_needsCompositing = true;
2228-
if (oldNeedsCompositing != _needsCompositing)
2322+
// If a node was previously a repaint boundary, but no longer is one, then
2323+
// regardless of its compositing state we need to find a new parent to
2324+
// paint from. To do this, we mark it clean again so that the traversal
2325+
// in markNeedsPaint is not short-circuited. It is removed from _nodesNeedingPaint
2326+
// so that we do not attempt to paint from it after locating a parent.
2327+
if (!isRepaintBoundary && _wasRepaintBoundary) {
2328+
_needsPaint = false;
2329+
_needsCompositedLayerUpdate = false;
2330+
owner?._nodesNeedingPaint.remove(this);
2331+
_needsCompositingBitsUpdate = false;
2332+
markNeedsPaint();
2333+
} else if (oldNeedsCompositing != _needsCompositing) {
2334+
_needsCompositingBitsUpdate = false;
22292335
markNeedsPaint();
2230-
_needsCompositingBitsUpdate = false;
2336+
} else {
2337+
_needsCompositingBitsUpdate = false;
2338+
}
22312339
}
22322340

22332341
/// Whether this render object's paint information is dirty.
@@ -2254,6 +2362,24 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
22542362
}
22552363
bool _needsPaint = true;
22562364

2365+
/// Whether this render object's layer information is dirty.
2366+
///
2367+
/// This is only set in debug mode. In general, render objects should not need
2368+
/// to condition their runtime behavior on whether they are dirty or not,
2369+
/// since they should only be marked dirty immediately prior to being laid
2370+
/// out and painted. (In release builds, this throws.)
2371+
///
2372+
/// It is intended to be used by tests and asserts.
2373+
bool get debugNeedsCompositedLayerUpdate {
2374+
late bool result;
2375+
assert(() {
2376+
result = _needsCompositedLayerUpdate;
2377+
return true;
2378+
}());
2379+
return result;
2380+
}
2381+
bool _needsCompositedLayerUpdate = false;
2382+
22572383
/// Mark this render object as having changed its visual appearance.
22582384
///
22592385
/// Rather than eagerly updating this render object's display list
@@ -2280,7 +2406,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
22802406
if (_needsPaint)
22812407
return;
22822408
_needsPaint = true;
2283-
if (isRepaintBoundary) {
2409+
// If this was not previously a repaint boundary it will not have
2410+
// a layer we can paint from.
2411+
if (isRepaintBoundary && _wasRepaintBoundary) {
22842412
assert(() {
22852413
if (debugPrintMarkNeedsPaintStacks)
22862414
debugPrintStack(label: 'markNeedsPaint() called for $this');
@@ -2312,6 +2440,45 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
23122440
}
23132441
}
23142442

2443+
/// Mark this render object as having changed a property on its composited
2444+
/// layer.
2445+
///
2446+
/// Render objects that have a composited layer have [isRepaintBoundary] equal
2447+
/// to true may update the properties of that composited layer without repainting
2448+
/// their children. If this render object is a repaint boundary but does
2449+
/// not yet have a composited layer created for it, this method will instead
2450+
/// mark the nearest repaint boundary parent as needing to be painted.
2451+
///
2452+
/// If this method is called on a render object that is not a repaint boundary
2453+
/// or is a repaint boundary but hasn't been composited yet, it is equivalent
2454+
/// to calling [markNeedsPaint].
2455+
///
2456+
/// See also:
2457+
///
2458+
/// * [RenderOpacity], which uses this method when its opacity is updated to
2459+
/// update the layer opacity without repainting children.
2460+
void markNeedsCompositedLayerUpdate() {
2461+
assert(!_debugDisposed);
2462+
assert(owner == null || !owner!.debugDoingPaint);
2463+
if (_needsCompositedLayerUpdate || _needsPaint) {
2464+
return;
2465+
}
2466+
_needsCompositedLayerUpdate = true;
2467+
// If this was not previously a repaint boundary it will not have
2468+
// a layer we can paint from.
2469+
if (isRepaintBoundary && _wasRepaintBoundary) {
2470+
// If we always have our own layer, then we can just repaint
2471+
// ourselves without involving any other nodes.
2472+
assert(_layerHandle.layer != null);
2473+
if (owner != null) {
2474+
owner!._nodesNeedingPaint.add(this);
2475+
owner!.requestVisualUpdate();
2476+
}
2477+
} else {
2478+
markNeedsPaint();
2479+
}
2480+
}
2481+
23152482
// Called when flushPaint() tries to make us paint but our layer is detached.
23162483
// To make sure that our subtree is repainted when it's finally reattached,
23172484
// even in the case where some ancestor layer is itself never marked dirty, we
@@ -2320,7 +2487,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
23202487
void _skippedPaintingOnLayer() {
23212488
assert(attached);
23222489
assert(isRepaintBoundary);
2323-
assert(_needsPaint);
2490+
assert(_needsPaint || _needsCompositedLayerUpdate);
23242491
assert(_layerHandle.layer != null);
23252492
assert(!_layerHandle.layer!.attached);
23262493
AbstractNode? node = parent;
@@ -2475,6 +2642,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
24752642
return true;
24762643
}());
24772644
_needsPaint = false;
2645+
_needsCompositedLayerUpdate = false;
2646+
_wasRepaintBoundary = isRepaintBoundary;
24782647
try {
24792648
paint(context, offset);
24802649
assert(!_needsLayout); // check that the paint() method didn't mark us dirty again

0 commit comments

Comments
 (0)