@@ -121,22 +121,38 @@ class PaintingContext extends ClipContext {
121
121
if (childLayer == null ) {
122
122
assert (debugAlsoPaintedParent);
123
123
assert (child._layerHandle.layer == null );
124
+
124
125
// Not using the `layer` setter because the setter asserts that we not
125
126
// replace the layer for repaint boundaries. That assertion does not
126
127
// apply here because this is exactly the place designed to create a
127
128
// layer for repaint boundaries.
128
- final OffsetLayer layer = OffsetLayer ( );
129
+ final OffsetLayer layer = child. updateCompositedLayer (oldLayer : null );
129
130
child._layerHandle.layer = childLayer = layer;
130
131
} else {
131
132
assert (debugAlsoPaintedParent || childLayer.attached);
133
+ Offset ? debugOldOffset;
134
+ assert (() {
135
+ debugOldOffset = childLayer! .offset;
136
+ return true ;
137
+ }());
132
138
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);
133
146
}
147
+ child._needsCompositedLayerUpdate = false ;
148
+
134
149
assert (identical (childLayer, child._layerHandle.layer));
135
150
assert (child._layerHandle.layer is OffsetLayer );
136
151
assert (() {
137
152
childLayer! .debugCreator = child.debugCreator ?? child.runtimeType;
138
153
return true ;
139
154
}());
155
+
140
156
childContext ?? = PaintingContext (childLayer, child.paintBounds);
141
157
child._paintWithContext (childContext, Offset .zero);
142
158
@@ -146,6 +162,38 @@ class PaintingContext extends ClipContext {
146
162
childContext.stopRecordingIfNeeded ();
147
163
}
148
164
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
+
149
197
/// In debug mode, repaint the given render object using a custom painting
150
198
/// context that can record the results of the painting operation in addition
151
199
/// to performing the regular paint of the child.
@@ -183,6 +231,12 @@ class PaintingContext extends ClipContext {
183
231
if (child.isRepaintBoundary) {
184
232
stopRecordingIfNeeded ();
185
233
_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);
186
240
} else {
187
241
child._paintWithContext (this , offset);
188
242
}
@@ -194,9 +248,12 @@ class PaintingContext extends ClipContext {
194
248
assert (_canvas == null || _canvas! .getSaveCount () == 1 );
195
249
196
250
// Create a layer for our child, and paint the child into it.
197
- if (child._needsPaint) {
251
+ if (child._needsPaint || ! child._wasRepaintBoundary ) {
198
252
repaintCompositedChild (child, debugAlsoPaintedParent: true );
199
253
} else {
254
+ if (child._needsCompositedLayerUpdate) {
255
+ updateLayerProperties (child);
256
+ }
200
257
assert (() {
201
258
// register the call for RepaintBoundary metrics
202
259
child.debugRegisterRepaintBoundaryPaint ();
@@ -978,19 +1035,25 @@ class PipelineOwner {
978
1035
arguments: debugTimelineArguments,
979
1036
);
980
1037
}
981
- assert (() {
982
- _debugDoingPaint = true ;
983
- return true ;
984
- }());
985
1038
try {
1039
+ assert (() {
1040
+ _debugDoingPaint = true ;
1041
+ return true ;
1042
+ }());
986
1043
final List <RenderObject > dirtyNodes = _nodesNeedingPaint;
987
1044
_nodesNeedingPaint = < RenderObject > [];
1045
+
988
1046
// Sort the dirty nodes in reverse order (deepest first).
989
1047
for (final RenderObject node in dirtyNodes..sort ((RenderObject a, RenderObject b) => b.depth - a.depth)) {
990
1048
assert (node._layerHandle.layer != null );
991
- if (node._needsPaint && node.owner == this ) {
1049
+ if (( node._needsPaint || node._needsCompositedLayerUpdate) && node.owner == this ) {
992
1050
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
+ }
994
1057
} else {
995
1058
node._skippedPaintingOnLayer ();
996
1059
}
@@ -1236,6 +1299,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
1236
1299
/// Initializes internal fields for subclasses.
1237
1300
RenderObject () {
1238
1301
_needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
1302
+ _wasRepaintBoundary = isRepaintBoundary;
1239
1303
}
1240
1304
1241
1305
/// Cause the entire subtree rooted at the given [RenderObject] to be marked
@@ -2070,12 +2134,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
2070
2134
/// to repaint.
2071
2135
///
2072
2136
/// 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.
2077
2141
///
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.
2079
2144
///
2080
2145
/// See [RepaintBoundary] for more information about how repaint boundaries function.
2081
2146
bool get isRepaintBoundary => false ;
@@ -2098,6 +2163,34 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
2098
2163
@protected
2099
2164
bool get alwaysNeedsCompositing => false ;
2100
2165
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
+
2101
2194
/// The compositing layer that this render object uses to repaint.
2102
2195
///
2103
2196
/// 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
2184
2277
final RenderObject parent = this .parent! as RenderObject ;
2185
2278
if (parent._needsCompositingBitsUpdate)
2186
2279
return ;
2187
- if (! isRepaintBoundary && ! parent.isRepaintBoundary) {
2280
+
2281
+ if ((! _wasRepaintBoundary || ! isRepaintBoundary) && ! parent.isRepaintBoundary) {
2188
2282
parent.markNeedsCompositingBitsUpdate ();
2189
2283
return ;
2190
2284
}
@@ -2225,9 +2319,23 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
2225
2319
});
2226
2320
if (isRepaintBoundary || alwaysNeedsCompositing)
2227
2321
_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 ;
2229
2335
markNeedsPaint ();
2230
- _needsCompositingBitsUpdate = false ;
2336
+ } else {
2337
+ _needsCompositingBitsUpdate = false ;
2338
+ }
2231
2339
}
2232
2340
2233
2341
/// Whether this render object's paint information is dirty.
@@ -2254,6 +2362,24 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
2254
2362
}
2255
2363
bool _needsPaint = true ;
2256
2364
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
+
2257
2383
/// Mark this render object as having changed its visual appearance.
2258
2384
///
2259
2385
/// Rather than eagerly updating this render object's display list
@@ -2280,7 +2406,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
2280
2406
if (_needsPaint)
2281
2407
return ;
2282
2408
_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) {
2284
2412
assert (() {
2285
2413
if (debugPrintMarkNeedsPaintStacks)
2286
2414
debugPrintStack (label: 'markNeedsPaint() called for $this ' );
@@ -2312,6 +2440,45 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
2312
2440
}
2313
2441
}
2314
2442
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
+
2315
2482
// Called when flushPaint() tries to make us paint but our layer is detached.
2316
2483
// To make sure that our subtree is repainted when it's finally reattached,
2317
2484
// 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
2320
2487
void _skippedPaintingOnLayer () {
2321
2488
assert (attached);
2322
2489
assert (isRepaintBoundary);
2323
- assert (_needsPaint);
2490
+ assert (_needsPaint || _needsCompositedLayerUpdate );
2324
2491
assert (_layerHandle.layer != null );
2325
2492
assert (! _layerHandle.layer! .attached);
2326
2493
AbstractNode ? node = parent;
@@ -2475,6 +2642,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
2475
2642
return true ;
2476
2643
}());
2477
2644
_needsPaint = false ;
2645
+ _needsCompositedLayerUpdate = false ;
2646
+ _wasRepaintBoundary = isRepaintBoundary;
2478
2647
try {
2479
2648
paint (context, offset);
2480
2649
assert (! _needsLayout); // check that the paint() method didn't mark us dirty again
0 commit comments